pyrixos-lattice 0.9.0__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.
- lattice/__init__.py +166 -0
- lattice/api.py +453 -0
- lattice/cache/__init__.py +8 -0
- lattice/cache/archive.py +194 -0
- lattice/cache/base.py +77 -0
- lattice/cache/cvmfs.py +100 -0
- lattice/cache/http.py +143 -0
- lattice/cache/metadata.py +147 -0
- lattice/cache/provenance.py +190 -0
- lattice/cache/push.py +200 -0
- lattice/cache/signing.py +107 -0
- lattice/cache/substitute.py +341 -0
- lattice/cli.py +568 -0
- lattice/client.py +438 -0
- lattice/derivation.py +280 -0
- lattice/errors.py +76 -0
- lattice/events.py +194 -0
- lattice/kmod/__init__.py +29 -0
- lattice/kmod/modinfo.py +168 -0
- lattice/kmod/syscalls.py +51 -0
- lattice/kmod/system.py +113 -0
- lattice/models.py +142 -0
- lattice/procscan.py +118 -0
- lattice/profiles/__init__.py +42 -0
- lattice/profiles/activation.py +254 -0
- lattice/profiles/generations.py +138 -0
- lattice/profiles/slots.py +64 -0
- lattice/protocol.py +256 -0
- lattice/py.typed +0 -0
- lattice/sandbox.py +281 -0
- lattice/server.py +268 -0
- lattice/settings.py +337 -0
- lattice/stdlib.py +181 -0
- lattice/store/__init__.py +36 -0
- lattice/store/builder.py +500 -0
- lattice/store/combine.py +199 -0
- lattice/store/database.py +429 -0
- lattice/store/fetch.py +97 -0
- lattice/store/gc.py +118 -0
- lattice/store/layout.py +123 -0
- lattice/store/lock.py +60 -0
- lattice/store/realise.py +172 -0
- lattice/store/references.py +70 -0
- lattice/store/verify.py +143 -0
- lattice/transport.py +412 -0
- pyrixos_lattice-0.9.0.dist-info/METADATA +357 -0
- pyrixos_lattice-0.9.0.dist-info/RECORD +51 -0
- pyrixos_lattice-0.9.0.dist-info/WHEEL +5 -0
- pyrixos_lattice-0.9.0.dist-info/entry_points.txt +2 -0
- pyrixos_lattice-0.9.0.dist-info/licenses/LICENSE +339 -0
- pyrixos_lattice-0.9.0.dist-info/top_level.txt +1 -0
lattice/__init__.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-2.0
|
|
2
|
+
"""Lattice, the PyrixOS package manager.
|
|
3
|
+
|
|
4
|
+
Lattice evaluates PyBonsai files with the ``pybonsai`` package, passing
|
|
5
|
+
its own standard library as the host (RFC 0001 sections 7.6 and 8).
|
|
6
|
+
|
|
7
|
+
Programs use Lattice through the names exported here (RFC 0001 sections
|
|
8
|
+
6.1 and 6.2). Lattice runs operations in the calling process, Client sends
|
|
9
|
+
them to ``lattice daemon`` over its Unix socket or the remote API, and
|
|
10
|
+
Daemon is the daemon itself. Lattice and Client both satisfy LatticeAPI.
|
|
11
|
+
The ``lattice`` command is a thin wrapper around them::
|
|
12
|
+
|
|
13
|
+
from lattice import Client, Lattice
|
|
14
|
+
|
|
15
|
+
result = Lattice().evaluate("hello.pyb")
|
|
16
|
+
result = Client().evaluate("hello.pyb")
|
|
17
|
+
|
|
18
|
+
Names in ``__all__`` follow semantic versioning from release 1.0. A name
|
|
19
|
+
is removed only after one minor release in which using it raises a
|
|
20
|
+
DeprecationWarning. Modules and names not listed here are internal.
|
|
21
|
+
|
|
22
|
+
Phase 1 provides derivations, the standard library and evaluation,
|
|
23
|
+
Phase 2 the build sandbox (``lattice.sandbox``, built on the Cloche
|
|
24
|
+
package), Phase 3 the store, the dependency database and the garbage
|
|
25
|
+
collector (``lattice.store``), and Phase 4 profiles, slots and kernel
|
|
26
|
+
modules (``lattice.profiles`` and ``lattice.kmod``). Phase 5 adds binary
|
|
27
|
+
caches.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from .api import Lattice
|
|
33
|
+
from .client import DEFAULT_URL, Client, LatticeAPI
|
|
34
|
+
from .derivation import Derivation
|
|
35
|
+
from .errors import (
|
|
36
|
+
ActivationFailed,
|
|
37
|
+
AuthenticationFailed,
|
|
38
|
+
BackendUnavailable,
|
|
39
|
+
BuildFailed,
|
|
40
|
+
EvaluationFailed,
|
|
41
|
+
LatticeError,
|
|
42
|
+
PermissionDenied,
|
|
43
|
+
ProtocolError,
|
|
44
|
+
SettingsError,
|
|
45
|
+
StoreError,
|
|
46
|
+
SubstitutionFailed,
|
|
47
|
+
)
|
|
48
|
+
from .events import (
|
|
49
|
+
ActivationDeferred,
|
|
50
|
+
BuildFinished,
|
|
51
|
+
BuildLog,
|
|
52
|
+
BuildStarted,
|
|
53
|
+
CacheSkipped,
|
|
54
|
+
CollectionFinished,
|
|
55
|
+
EvaluationFinished,
|
|
56
|
+
EvaluationStarted,
|
|
57
|
+
Event,
|
|
58
|
+
EventHandler,
|
|
59
|
+
KernelModuleLoaded,
|
|
60
|
+
KernelModuleUnloaded,
|
|
61
|
+
PathPushed,
|
|
62
|
+
ProfileSwitched,
|
|
63
|
+
SlotChanged,
|
|
64
|
+
SubstitutionFinished,
|
|
65
|
+
SubstitutionStarted,
|
|
66
|
+
)
|
|
67
|
+
from .kmod import KernelModules, LinuxModules, LoadedModule, ModuleInfo
|
|
68
|
+
from .models import (
|
|
69
|
+
BuildResult,
|
|
70
|
+
Evaluation,
|
|
71
|
+
GCResult,
|
|
72
|
+
Generation,
|
|
73
|
+
Identity,
|
|
74
|
+
PushResult,
|
|
75
|
+
Role,
|
|
76
|
+
SlotState,
|
|
77
|
+
SubstitutionResult,
|
|
78
|
+
)
|
|
79
|
+
from .sandbox import BackendStatus, OciConfig
|
|
80
|
+
from .settings import (
|
|
81
|
+
DEFAULT_SOCKET,
|
|
82
|
+
SANDBOX_BACKENDS,
|
|
83
|
+
CacheSettings,
|
|
84
|
+
ClientGrant,
|
|
85
|
+
DaemonSettings,
|
|
86
|
+
MicroVMSettings,
|
|
87
|
+
SandboxBackend,
|
|
88
|
+
SandboxSettings,
|
|
89
|
+
Settings,
|
|
90
|
+
ShallowRules,
|
|
91
|
+
StoreSettings,
|
|
92
|
+
TokenGrant,
|
|
93
|
+
)
|
|
94
|
+
from .store import BuildPlan, GCRootInfo, PlannedStep, StorePathInfo, VerifyResult
|
|
95
|
+
from .transport import Daemon
|
|
96
|
+
|
|
97
|
+
__version__ = "0.9.0"
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
"ActivationDeferred",
|
|
101
|
+
"ActivationFailed",
|
|
102
|
+
"KernelModuleLoaded",
|
|
103
|
+
"KernelModuleUnloaded",
|
|
104
|
+
"KernelModules",
|
|
105
|
+
"LinuxModules",
|
|
106
|
+
"LoadedModule",
|
|
107
|
+
"ModuleInfo",
|
|
108
|
+
"ProfileSwitched",
|
|
109
|
+
"SlotChanged",
|
|
110
|
+
"SlotState",
|
|
111
|
+
"VerifyResult",
|
|
112
|
+
"StoreSettings",
|
|
113
|
+
"StorePathInfo",
|
|
114
|
+
"PlannedStep",
|
|
115
|
+
"GCRootInfo",
|
|
116
|
+
"CollectionFinished",
|
|
117
|
+
"BuildStarted",
|
|
118
|
+
"BuildPlan",
|
|
119
|
+
"BuildLog",
|
|
120
|
+
"BuildFinished",
|
|
121
|
+
"DEFAULT_SOCKET",
|
|
122
|
+
"DEFAULT_URL",
|
|
123
|
+
"SANDBOX_BACKENDS",
|
|
124
|
+
"AuthenticationFailed",
|
|
125
|
+
"BackendStatus",
|
|
126
|
+
"BackendUnavailable",
|
|
127
|
+
"BuildFailed",
|
|
128
|
+
"BuildResult",
|
|
129
|
+
"Client",
|
|
130
|
+
"ClientGrant",
|
|
131
|
+
"Daemon",
|
|
132
|
+
"DaemonSettings",
|
|
133
|
+
"Derivation",
|
|
134
|
+
"Evaluation",
|
|
135
|
+
"EvaluationFailed",
|
|
136
|
+
"EvaluationFinished",
|
|
137
|
+
"EvaluationStarted",
|
|
138
|
+
"Event",
|
|
139
|
+
"EventHandler",
|
|
140
|
+
"GCResult",
|
|
141
|
+
"Generation",
|
|
142
|
+
"Identity",
|
|
143
|
+
"Lattice",
|
|
144
|
+
"LatticeAPI",
|
|
145
|
+
"LatticeError",
|
|
146
|
+
"MicroVMSettings",
|
|
147
|
+
"OciConfig",
|
|
148
|
+
"PermissionDenied",
|
|
149
|
+
"ProtocolError",
|
|
150
|
+
"Role",
|
|
151
|
+
"SandboxBackend",
|
|
152
|
+
"SandboxSettings",
|
|
153
|
+
"Settings",
|
|
154
|
+
"SettingsError",
|
|
155
|
+
"StoreError",
|
|
156
|
+
"SubstitutionFailed",
|
|
157
|
+
"TokenGrant",
|
|
158
|
+
"CacheSettings",
|
|
159
|
+
"CacheSkipped",
|
|
160
|
+
"PathPushed",
|
|
161
|
+
"PushResult",
|
|
162
|
+
"ShallowRules",
|
|
163
|
+
"SubstitutionFinished",
|
|
164
|
+
"SubstitutionResult",
|
|
165
|
+
"SubstitutionStarted",
|
|
166
|
+
]
|
lattice/api.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-2.0
|
|
2
|
+
"""The Lattice library interface (RFC 0001 section 6.1).
|
|
3
|
+
|
|
4
|
+
Lattice is the class a program creates to use the package manager from
|
|
5
|
+
Python. The ``lattice`` command is a thin wrapper around it, so anything
|
|
6
|
+
the command does, a program can do too. A Lattice object holds only its
|
|
7
|
+
settings and event handler; it keeps no state between calls and reads no
|
|
8
|
+
settings from the machine on its own.
|
|
9
|
+
|
|
10
|
+
Methods return immutable result models (models.py) and raise LatticeError
|
|
11
|
+
subclasses. Client (client.py) has the same methods and talks to the
|
|
12
|
+
daemon instead; both satisfy the LatticeAPI protocol. Each method states
|
|
13
|
+
the phase of the development plan that implemented it.
|
|
14
|
+
|
|
15
|
+
Example::
|
|
16
|
+
|
|
17
|
+
from lattice import Lattice, Settings
|
|
18
|
+
|
|
19
|
+
lattice = Lattice(Settings(sandbox_backend="namespaces"))
|
|
20
|
+
result = lattice.evaluate("hello.pyb")
|
|
21
|
+
for name, drv in result.derivations.items():
|
|
22
|
+
print(name, drv.store_path)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import sqlite3
|
|
29
|
+
import time
|
|
30
|
+
from collections.abc import Iterable
|
|
31
|
+
from platform import machine
|
|
32
|
+
from typing import TYPE_CHECKING
|
|
33
|
+
|
|
34
|
+
import pybonsai
|
|
35
|
+
from pybonsai import FrozenMap, Host, PyBonsaiError
|
|
36
|
+
|
|
37
|
+
from . import profiles
|
|
38
|
+
from .derivation import BUILTIN_PROFILE, Derivation
|
|
39
|
+
from .errors import ActivationFailed, EvaluationFailed, StoreError
|
|
40
|
+
from .events import EvaluationFinished, EvaluationStarted, EventHandler, ProfileSwitched, emit
|
|
41
|
+
from .kmod import KernelModules, LinuxModules
|
|
42
|
+
from .models import (
|
|
43
|
+
BuildResult,
|
|
44
|
+
Evaluation,
|
|
45
|
+
GCResult,
|
|
46
|
+
Generation,
|
|
47
|
+
PushResult,
|
|
48
|
+
SlotState,
|
|
49
|
+
SubstitutionResult,
|
|
50
|
+
)
|
|
51
|
+
from .sandbox import BackendStatus, OciConfig, make_spec, oci_config
|
|
52
|
+
from .sandbox import backend_status as probe_backends
|
|
53
|
+
from .settings import SandboxBackend, Settings
|
|
54
|
+
from .stdlib import build_host
|
|
55
|
+
from .store import (
|
|
56
|
+
Builder,
|
|
57
|
+
BuildPlan,
|
|
58
|
+
GCRootInfo,
|
|
59
|
+
StoreLayout,
|
|
60
|
+
StorePathInfo,
|
|
61
|
+
VerifyResult,
|
|
62
|
+
check_store_path,
|
|
63
|
+
collect,
|
|
64
|
+
content_hash,
|
|
65
|
+
database,
|
|
66
|
+
lock,
|
|
67
|
+
)
|
|
68
|
+
from .store.builder import direct_inputs
|
|
69
|
+
from .store.combine import profile_manifest
|
|
70
|
+
|
|
71
|
+
if TYPE_CHECKING:
|
|
72
|
+
from .sandbox import Backend
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Lattice:
|
|
76
|
+
"""Entry point for using Lattice from Python.
|
|
77
|
+
|
|
78
|
+
``settings`` defaults to Settings(). ``on_event`` receives an Event for
|
|
79
|
+
each step of an operation (see events.py). ``backend`` is the sandbox
|
|
80
|
+
backend for builds; None chooses the one the settings name when the
|
|
81
|
+
first build needs it. ``kernel_modules`` loads and unloads the kernel
|
|
82
|
+
modules of stacks; None uses the running Linux kernel.
|
|
83
|
+
|
|
84
|
+
The store under ``settings.store.root`` is created by the first
|
|
85
|
+
operation that writes to it. Operations that only read it raise
|
|
86
|
+
StoreError while it does not exist.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
settings: Settings | None = None,
|
|
92
|
+
*,
|
|
93
|
+
on_event: EventHandler | None = None,
|
|
94
|
+
backend: Backend | None = None,
|
|
95
|
+
kernel_modules: KernelModules | None = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Store the settings, event handler and backends. Nothing is read or created."""
|
|
98
|
+
self.settings = settings if settings is not None else Settings()
|
|
99
|
+
self.on_event = on_event
|
|
100
|
+
self.backend = backend
|
|
101
|
+
self.kernel_modules = kernel_modules if kernel_modules is not None else LinuxModules()
|
|
102
|
+
self.layout = StoreLayout(root=self.settings.store.root, stacks=self.settings.store.stacks)
|
|
103
|
+
self._host: Host | None = None
|
|
104
|
+
|
|
105
|
+
def _get_host(self) -> Host:
|
|
106
|
+
if self._host is None:
|
|
107
|
+
self._host = build_host()
|
|
108
|
+
return self._host
|
|
109
|
+
|
|
110
|
+
def evaluate(self, path: str | os.PathLike[str]) -> Evaluation:
|
|
111
|
+
"""Evaluate the PyBonsai file at ``path`` with the standard library. Phase 1.
|
|
112
|
+
|
|
113
|
+
Nothing is built. Raises EvaluationFailed if the file is rejected or
|
|
114
|
+
fails, and OSError if it cannot be read.
|
|
115
|
+
"""
|
|
116
|
+
filename = os.fspath(path)
|
|
117
|
+
emit(self.on_event, EvaluationStarted(filename=filename))
|
|
118
|
+
start = time.perf_counter()
|
|
119
|
+
try:
|
|
120
|
+
names = pybonsai.evaluate_file(path, host=self._get_host())
|
|
121
|
+
except PyBonsaiError as error:
|
|
122
|
+
raise EvaluationFailed(error) from error
|
|
123
|
+
return self._finish(filename, names, start)
|
|
124
|
+
|
|
125
|
+
def evaluate_source(self, source: str, filename: str = "<string>") -> Evaluation:
|
|
126
|
+
"""Evaluate PyBonsai source text, as evaluate does for a file. Phase 1."""
|
|
127
|
+
emit(self.on_event, EvaluationStarted(filename=filename))
|
|
128
|
+
start = time.perf_counter()
|
|
129
|
+
try:
|
|
130
|
+
names = pybonsai.evaluate_source(source, filename, host=self._get_host())
|
|
131
|
+
except PyBonsaiError as error:
|
|
132
|
+
raise EvaluationFailed(error) from error
|
|
133
|
+
return self._finish(filename, names, start)
|
|
134
|
+
|
|
135
|
+
def _finish(self, filename: str, names: FrozenMap, start: float) -> Evaluation:
|
|
136
|
+
derivations = {str(k): v for k, v in names.items() if isinstance(v, Derivation)}
|
|
137
|
+
result = Evaluation(
|
|
138
|
+
filename=filename, derivations=derivations, seconds=time.perf_counter() - start
|
|
139
|
+
)
|
|
140
|
+
emit(
|
|
141
|
+
self.on_event,
|
|
142
|
+
EvaluationFinished(
|
|
143
|
+
filename=filename, derivations=len(derivations), seconds=result.seconds
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
def _builder(self) -> Builder:
|
|
149
|
+
return Builder(self.layout, self.settings, backend=self.backend, on_event=self.on_event)
|
|
150
|
+
|
|
151
|
+
def build(self, drv: Derivation) -> BuildResult:
|
|
152
|
+
"""Realise ``drv`` and its inputs and register the outputs. Phase 3.
|
|
153
|
+
|
|
154
|
+
Outputs already registered are reused, and from Phase 5 paths in
|
|
155
|
+
a trusted cache are substituted. The outputs are not made roots,
|
|
156
|
+
so the next collection removes them unless add_root keeps them.
|
|
157
|
+
Raises BuildFailed, BackendUnavailable or StoreError.
|
|
158
|
+
"""
|
|
159
|
+
return self._builder().build(drv)
|
|
160
|
+
|
|
161
|
+
def check(self, drv: Derivation, backend: SandboxBackend | None = None) -> BuildResult:
|
|
162
|
+
"""Rebuild the registered outputs of ``drv`` and compare them (section 16). Phase 3.
|
|
163
|
+
|
|
164
|
+
``backend`` rebuilds on that sandbox backend instead of the
|
|
165
|
+
configured one. The rebuilt outputs are thrown away. Raises
|
|
166
|
+
StoreError if ``drv`` is not built and BuildFailed, naming the
|
|
167
|
+
files that differ, if the outputs are not the same.
|
|
168
|
+
"""
|
|
169
|
+
return self._builder().check(drv, backend)
|
|
170
|
+
|
|
171
|
+
def plan(self, drv: Derivation) -> BuildPlan:
|
|
172
|
+
"""List what building ``drv`` would download, build or reuse. Phase 3."""
|
|
173
|
+
return self._builder().plan(drv)
|
|
174
|
+
|
|
175
|
+
def _connect(self) -> sqlite3.Connection:
|
|
176
|
+
return database.connect(self.layout)
|
|
177
|
+
|
|
178
|
+
def path_info(self, path: str) -> StorePathInfo:
|
|
179
|
+
"""Return what the store database records about ``path``. Phase 3.
|
|
180
|
+
|
|
181
|
+
Raises StoreError if ``path`` is not a registered store path.
|
|
182
|
+
"""
|
|
183
|
+
check_store_path(path)
|
|
184
|
+
connection = self._connect()
|
|
185
|
+
try:
|
|
186
|
+
info = database.path_info(connection, path)
|
|
187
|
+
finally:
|
|
188
|
+
connection.close()
|
|
189
|
+
if info is None:
|
|
190
|
+
raise StoreError(f"{path} is not registered")
|
|
191
|
+
return info
|
|
192
|
+
|
|
193
|
+
def verify(self, path: str) -> VerifyResult:
|
|
194
|
+
"""Hash the contents of ``path`` again and compare with the database (section 11).
|
|
195
|
+
|
|
196
|
+
Phase 3. A shallow path is read through its link, which fetches
|
|
197
|
+
every file from CernVM-FS (section 14.6). Raises StoreError if
|
|
198
|
+
``path`` is not registered or its files are missing.
|
|
199
|
+
"""
|
|
200
|
+
self.path_info(path)
|
|
201
|
+
with lock.shared(self.layout):
|
|
202
|
+
info = self.path_info(path)
|
|
203
|
+
physical = self.layout.physical(path)
|
|
204
|
+
if info.location == "shallow" and os.path.islink(physical):
|
|
205
|
+
physical = os.readlink(physical)
|
|
206
|
+
try:
|
|
207
|
+
digest, size = content_hash(physical)
|
|
208
|
+
except FileNotFoundError:
|
|
209
|
+
raise StoreError(f"{path} is registered but its files are missing") from None
|
|
210
|
+
return VerifyResult(
|
|
211
|
+
path=path,
|
|
212
|
+
ok=digest == info.content_sha256 and size == info.size,
|
|
213
|
+
expected_sha256=info.content_sha256,
|
|
214
|
+
actual_sha256=digest,
|
|
215
|
+
expected_size=info.size,
|
|
216
|
+
actual_size=size,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def sandbox_config(self, drv: Derivation) -> OciConfig:
|
|
220
|
+
"""Describe the sandbox that would build ``drv`` as an OCI runtime configuration.
|
|
221
|
+
|
|
222
|
+
Uses the sandbox settings and this process's uid and gid; nothing
|
|
223
|
+
runs. When the inputs of ``drv`` are registered, the sandbox has
|
|
224
|
+
them and the paths they refer to, as a build would; otherwise it
|
|
225
|
+
has the outputs of every derivation ``drv`` depends on. Raises
|
|
226
|
+
BuildFailed if ``drv`` cannot be built in the sandbox and
|
|
227
|
+
BackendUnavailable if the machine is not supported. Phase 2.
|
|
228
|
+
"""
|
|
229
|
+
spec = make_spec(
|
|
230
|
+
drv,
|
|
231
|
+
self.settings.sandbox,
|
|
232
|
+
uid=os.getuid(),
|
|
233
|
+
gid=os.getgid(),
|
|
234
|
+
machine=machine(),
|
|
235
|
+
inputs=self._registered_inputs(drv),
|
|
236
|
+
)
|
|
237
|
+
return oci_config(spec)
|
|
238
|
+
|
|
239
|
+
def _registered_inputs(self, drv: Derivation) -> tuple[str, ...] | None:
|
|
240
|
+
if not self.layout.exists():
|
|
241
|
+
return None
|
|
242
|
+
direct = direct_inputs(drv)
|
|
243
|
+
connection = self._connect()
|
|
244
|
+
try:
|
|
245
|
+
found = database.closure(connection, direct)
|
|
246
|
+
finally:
|
|
247
|
+
connection.close()
|
|
248
|
+
return tuple(sorted(found)) if set(direct) <= found else None
|
|
249
|
+
|
|
250
|
+
def backend_status(self) -> tuple[BackendStatus, ...]:
|
|
251
|
+
"""Report which sandbox backends can run on this machine (section 9.7). Phase 2."""
|
|
252
|
+
return probe_backends(self.settings.sandbox, self.layout.store)
|
|
253
|
+
|
|
254
|
+
def gc(self) -> GCResult:
|
|
255
|
+
"""Remove every store path no root or running process keeps (section 12). Phase 3."""
|
|
256
|
+
return collect(self.layout, self.on_event)
|
|
257
|
+
|
|
258
|
+
def add_root(self, name: str, path: str) -> GCRootInfo:
|
|
259
|
+
"""Keep ``path`` and everything it refers to under the root ``name``. Phase 3.
|
|
260
|
+
|
|
261
|
+
A root of the same name is replaced. Raises StoreError if the name
|
|
262
|
+
is not valid or ``path`` is not registered.
|
|
263
|
+
"""
|
|
264
|
+
check_store_path(path)
|
|
265
|
+
connection = database.connect(self.layout, create=True)
|
|
266
|
+
try:
|
|
267
|
+
# Shared, so that a collection that has already marked path dead cannot be running.
|
|
268
|
+
with lock.shared(self.layout):
|
|
269
|
+
return database.add_root(connection, name, path)
|
|
270
|
+
finally:
|
|
271
|
+
connection.close()
|
|
272
|
+
|
|
273
|
+
def remove_root(self, name: str) -> GCRootInfo:
|
|
274
|
+
"""Remove the root ``name`` and return it. Phase 3.
|
|
275
|
+
|
|
276
|
+
Raises StoreError if there is no such root.
|
|
277
|
+
"""
|
|
278
|
+
connection = self._connect()
|
|
279
|
+
try:
|
|
280
|
+
return database.remove_root(connection, name)
|
|
281
|
+
finally:
|
|
282
|
+
connection.close()
|
|
283
|
+
|
|
284
|
+
def roots(self) -> tuple[GCRootInfo, ...]:
|
|
285
|
+
"""Return every root, sorted by name. Phase 3."""
|
|
286
|
+
if not self.layout.exists():
|
|
287
|
+
return ()
|
|
288
|
+
connection = self._connect()
|
|
289
|
+
try:
|
|
290
|
+
return database.roots(connection)
|
|
291
|
+
finally:
|
|
292
|
+
connection.close()
|
|
293
|
+
|
|
294
|
+
def switch(self, profile: str, drv: Derivation) -> Generation:
|
|
295
|
+
"""Build ``drv`` and make it the current generation of ``profile``. Phase 4.
|
|
296
|
+
|
|
297
|
+
The new generation gets the next number and a GC root. For the
|
|
298
|
+
``system`` profile the slots then change to the stacks it assigns
|
|
299
|
+
(section 13.3); if they cannot change now, an ActivationDeferred
|
|
300
|
+
event says why and they change at the next boot. Raises
|
|
301
|
+
ActivationFailed if a kernel module step fails, leaving the
|
|
302
|
+
previous generation current, and if a profile other than
|
|
303
|
+
``system`` assigns stacks.
|
|
304
|
+
"""
|
|
305
|
+
profiles.check_profile_name(profile)
|
|
306
|
+
if profile != profiles.SYSTEM and drv.builder == BUILTIN_PROFILE:
|
|
307
|
+
if profile_manifest(drv).stacks:
|
|
308
|
+
raise ActivationFailed(f"only the {profiles.SYSTEM} profile may assign stacks")
|
|
309
|
+
connection = database.connect(self.layout, create=True)
|
|
310
|
+
try:
|
|
311
|
+
# Shared from the build until the root exists, so collection cannot remove the output.
|
|
312
|
+
with lock.shared(self.layout):
|
|
313
|
+
path = self.build(drv).store_path
|
|
314
|
+
with lock.profiles(self.layout):
|
|
315
|
+
profiles.prepare(self.layout, profile)
|
|
316
|
+
number = max(profiles.numbers(self.layout, profile), default=0) + 1
|
|
317
|
+
database.add_root(connection, profiles.root_name(profile, number), path)
|
|
318
|
+
profiles.replace_link(profiles.link_path(self.layout, profile, number), path)
|
|
319
|
+
return self._make_current(profile, number, path)
|
|
320
|
+
finally:
|
|
321
|
+
connection.close()
|
|
322
|
+
|
|
323
|
+
def _make_current(self, profile: str, number: int, path: str) -> Generation:
|
|
324
|
+
"""Activate generation ``number`` if it is ``system``'s, then point the profile at it."""
|
|
325
|
+
stacks = profiles.wanted(self.layout, path)
|
|
326
|
+
if profile == profiles.SYSTEM:
|
|
327
|
+
profiles.activate(self.layout, self.kernel_modules, stacks, self.on_event)
|
|
328
|
+
elif stacks:
|
|
329
|
+
raise ActivationFailed(f"only the {profiles.SYSTEM} profile may assign stacks")
|
|
330
|
+
generation_link = profiles.link_path(self.layout, profile, number)
|
|
331
|
+
profiles.replace_link(
|
|
332
|
+
profiles.link_path(self.layout, profile), os.path.basename(generation_link)
|
|
333
|
+
)
|
|
334
|
+
emit(self.on_event, ProfileSwitched(profile=profile, number=number, store_path=path))
|
|
335
|
+
return Generation(profile=profile, number=number, store_path=path, current=True)
|
|
336
|
+
|
|
337
|
+
def rollback(self, profile: str, generation: int | None = None) -> Generation:
|
|
338
|
+
"""Make an existing generation of ``profile`` current again. Phase 4.
|
|
339
|
+
|
|
340
|
+
``generation`` defaults to the newest one older than the current
|
|
341
|
+
generation. Activation is as for switch. Raises StoreError if the
|
|
342
|
+
generation does not exist.
|
|
343
|
+
"""
|
|
344
|
+
profiles.check_profile_name(profile)
|
|
345
|
+
if not self.layout.exists():
|
|
346
|
+
raise StoreError(f"no store at {self.layout.root}")
|
|
347
|
+
with lock.profiles(self.layout):
|
|
348
|
+
if generation is None:
|
|
349
|
+
now = profiles.current(self.layout, profile)
|
|
350
|
+
earlier = [
|
|
351
|
+
n for n in profiles.numbers(self.layout, profile) if now is None or n < now
|
|
352
|
+
]
|
|
353
|
+
if not earlier:
|
|
354
|
+
raise StoreError(f"profile {profile!r} has no earlier generation")
|
|
355
|
+
generation = earlier[-1]
|
|
356
|
+
path = profiles.store_path(self.layout, profile, generation)
|
|
357
|
+
return self._make_current(profile, generation, path)
|
|
358
|
+
|
|
359
|
+
def generations(self, profile: str) -> tuple[Generation, ...]:
|
|
360
|
+
"""Return every generation of ``profile``, oldest first. Phase 4."""
|
|
361
|
+
profiles.check_profile_name(profile)
|
|
362
|
+
return profiles.generations(self.layout, profile)
|
|
363
|
+
|
|
364
|
+
def delete_generations(self, profile: str, keep: int) -> tuple[Generation, ...]:
|
|
365
|
+
"""Delete all but the newest ``keep`` generations and the current one. Phase 4.
|
|
366
|
+
|
|
367
|
+
Their roots are removed, so the next collection can reclaim what
|
|
368
|
+
only they used. Returns the deleted generations.
|
|
369
|
+
"""
|
|
370
|
+
profiles.check_profile_name(profile)
|
|
371
|
+
if keep < 0:
|
|
372
|
+
raise StoreError("keep must not be negative")
|
|
373
|
+
if not self.layout.exists():
|
|
374
|
+
return ()
|
|
375
|
+
connection = self._connect()
|
|
376
|
+
try:
|
|
377
|
+
with lock.profiles(self.layout):
|
|
378
|
+
existing = profiles.generations(self.layout, profile)
|
|
379
|
+
kept = (
|
|
380
|
+
{g.number for g in existing[max(len(existing) - keep, 0) :]} if keep else set()
|
|
381
|
+
)
|
|
382
|
+
deleted = tuple(g for g in existing if g.number not in kept and not g.current)
|
|
383
|
+
for generation in deleted:
|
|
384
|
+
os.unlink(profiles.link_path(self.layout, profile, generation.number))
|
|
385
|
+
try:
|
|
386
|
+
database.remove_root(
|
|
387
|
+
connection, profiles.root_name(profile, generation.number)
|
|
388
|
+
)
|
|
389
|
+
except StoreError:
|
|
390
|
+
pass
|
|
391
|
+
return deleted
|
|
392
|
+
finally:
|
|
393
|
+
connection.close()
|
|
394
|
+
|
|
395
|
+
def slots(self) -> tuple[SlotState, ...]:
|
|
396
|
+
"""Return every slot that is active or that the current system generation wants. Phase 4."""
|
|
397
|
+
return profiles.states(self.layout)
|
|
398
|
+
|
|
399
|
+
def activate_boot(self) -> tuple[SlotState, ...]:
|
|
400
|
+
"""Load the kernel modules and create the slots of the current system generation. Phase 4.
|
|
401
|
+
|
|
402
|
+
Runs once at boot, before any service starts. Library and command
|
|
403
|
+
only; the daemon does not offer it. Raises ActivationFailed after
|
|
404
|
+
setting every slot if a module did not load.
|
|
405
|
+
"""
|
|
406
|
+
if self.layout.exists():
|
|
407
|
+
with lock.profiles(self.layout):
|
|
408
|
+
wanted = profiles.wanted(self.layout, profiles.system_environment(self.layout))
|
|
409
|
+
profiles.boot(self.layout, self.kernel_modules, wanted, self.on_event)
|
|
410
|
+
else:
|
|
411
|
+
profiles.boot(self.layout, self.kernel_modules, {}, self.on_event)
|
|
412
|
+
return profiles.states(self.layout)
|
|
413
|
+
|
|
414
|
+
def substitute(self, path: str, shallow: bool = False) -> SubstitutionResult:
|
|
415
|
+
"""Fetch ``path`` and its missing references from the caches (section 14.1). Phase 5.
|
|
416
|
+
|
|
417
|
+
If ``shallow`` and the cache is a CernVM-FS repository, ``path``
|
|
418
|
+
becomes a link into it; its references are full copies. Raises
|
|
419
|
+
SubstitutionFailed if no trusted cache has a valid entry for a
|
|
420
|
+
path or a fetched path does not match its signed entry.
|
|
421
|
+
"""
|
|
422
|
+
from .cache.substitute import substitute
|
|
423
|
+
|
|
424
|
+
check_store_path(path)
|
|
425
|
+
return substitute(self.layout, self.settings, path, shallow=shallow, on_event=self.on_event)
|
|
426
|
+
|
|
427
|
+
def materialise(self, path: str) -> StorePathInfo:
|
|
428
|
+
"""Turn the shallow path ``path`` into a full copy (section 14.6). Phase 5.
|
|
429
|
+
|
|
430
|
+
A process that opens ``path`` while its link is replaced gets
|
|
431
|
+
ENOENT, so materialise a stack while its slot is not active.
|
|
432
|
+
Raises StoreError if ``path`` is not registered or its copy does
|
|
433
|
+
not match the registered hash.
|
|
434
|
+
"""
|
|
435
|
+
from .cache.substitute import materialise
|
|
436
|
+
|
|
437
|
+
check_store_path(path)
|
|
438
|
+
return materialise(self.layout, path)
|
|
439
|
+
|
|
440
|
+
def push(self, paths: Iterable[str], cache: str) -> PushResult:
|
|
441
|
+
"""Sign and publish store paths, their closures and provenance to ``cache``. Phase 5.
|
|
442
|
+
|
|
443
|
+
Paths the cache already has are skipped. Raises SettingsError if
|
|
444
|
+
no secret key is set, StoreError if a path was not built on this
|
|
445
|
+
machine or has changed since it was registered, and
|
|
446
|
+
SubstitutionFailed if the cache cannot be written (section 14.4).
|
|
447
|
+
"""
|
|
448
|
+
from .cache.push import push
|
|
449
|
+
|
|
450
|
+
paths = tuple(paths)
|
|
451
|
+
for path in paths:
|
|
452
|
+
check_store_path(path)
|
|
453
|
+
return push(self.layout, self.settings, paths, cache, on_event=self.on_event)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-2.0
|
|
2
|
+
"""Binary caches: signed archives of store paths (RFC 0001 section 14).
|
|
3
|
+
|
|
4
|
+
archive.py makes and unpacks deterministic archives, signing.py holds the
|
|
5
|
+
Ed25519 keys, metadata.py the files a cache serves, provenance.py the
|
|
6
|
+
signed build records, http.py and cvmfs.py the two kinds of cache,
|
|
7
|
+
substitute.py fetches paths from caches and push.py uploads them.
|
|
8
|
+
"""
|