omdev 0.0.0.dev223__py3-none-any.whl → 0.0.0.dev225__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- omdev/amalg/typing.py +7 -1
- omdev/ci/cache.py +108 -0
- omdev/ci/ci.py +1 -1
- omdev/ci/docker/cacheserved.py +262 -0
- omdev/ci/docker/dataserver.py +204 -0
- omdev/ci/docker/imagepulling.py +2 -1
- omdev/ci/docker/packing.py +72 -0
- omdev/ci/docker/repositories.py +40 -0
- omdev/ci/github/cache.py +20 -1
- omdev/ci/github/client.py +9 -2
- omdev/ci/github/inject.py +4 -4
- omdev/ci/requirements.py +1 -1
- omdev/ci/utils.py +0 -49
- omdev/dataserver/targets.py +32 -0
- omdev/git/revisions.py +2 -2
- omdev/git/shallow.py +1 -1
- omdev/git/status.py +1 -1
- omdev/oci/data.py +19 -0
- omdev/oci/dataserver.py +4 -1
- omdev/oci/pack/__init__.py +0 -0
- omdev/oci/pack/packing.py +185 -0
- omdev/oci/pack/repositories.py +162 -0
- omdev/oci/{packing.py → pack/unpacking.py} +0 -177
- omdev/oci/repositories.py +6 -0
- omdev/precheck/lite.py +1 -1
- omdev/pyproject/pkg.py +1 -1
- omdev/scripts/ci.py +773 -552
- omdev/scripts/interp.py +230 -299
- omdev/scripts/pyproject.py +328 -256
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/RECORD +35 -28
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev223.dist-info → omdev-0.0.0.dev225.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import contextlib
|
4
|
+
import shlex
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
|
8
|
+
from omlish.lite.timing import log_timing_context
|
9
|
+
from omlish.os.temp import temp_dir_context
|
10
|
+
|
11
|
+
from ...oci.repositories import DirectoryOciRepository
|
12
|
+
from ...oci.repositories import OciRepository
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
|
17
|
+
|
18
|
+
class DockerImageRepositoryOpener(abc.ABC):
|
19
|
+
@abc.abstractmethod
|
20
|
+
def open_docker_image_repository(self, image: str) -> ta.AsyncContextManager[OciRepository]:
|
21
|
+
raise NotImplementedError
|
22
|
+
|
23
|
+
|
24
|
+
#
|
25
|
+
|
26
|
+
|
27
|
+
class DockerImageRepositoryOpenerImpl(DockerImageRepositoryOpener):
|
28
|
+
@contextlib.asynccontextmanager
|
29
|
+
async def open_docker_image_repository(self, image: str) -> ta.AsyncGenerator[OciRepository, None]:
|
30
|
+
with temp_dir_context() as save_dir:
|
31
|
+
with log_timing_context(f'Saving docker image {image}'):
|
32
|
+
await asyncio_subprocesses.check_call(
|
33
|
+
' | '.join([
|
34
|
+
f'docker save {shlex.quote(image)}',
|
35
|
+
f'tar x -C {shlex.quote(save_dir)}',
|
36
|
+
]),
|
37
|
+
shell=True,
|
38
|
+
)
|
39
|
+
|
40
|
+
yield DirectoryOciRepository(save_dir)
|
omdev/ci/github/cache.py
CHANGED
@@ -7,8 +7,10 @@ from omlish.lite.check import check
|
|
7
7
|
from omlish.os.files import unlinking_if_exists
|
8
8
|
|
9
9
|
from ..cache import CacheVersion
|
10
|
+
from ..cache import DataCache
|
10
11
|
from ..cache import DirectoryFileCache
|
11
12
|
from ..cache import FileCache
|
13
|
+
from ..cache import FileCacheDataCache
|
12
14
|
from .client import GithubCacheClient
|
13
15
|
from .client import GithubCacheServiceV1Client
|
14
16
|
|
@@ -16,7 +18,7 @@ from .client import GithubCacheServiceV1Client
|
|
16
18
|
##
|
17
19
|
|
18
20
|
|
19
|
-
class
|
21
|
+
class GithubCache(FileCache, DataCache):
|
20
22
|
@dc.dataclass(frozen=True)
|
21
23
|
class Config:
|
22
24
|
dir: str
|
@@ -47,6 +49,8 @@ class GithubFileCache(FileCache):
|
|
47
49
|
version=self._version,
|
48
50
|
)
|
49
51
|
|
52
|
+
#
|
53
|
+
|
50
54
|
async def get_file(self, key: str) -> ta.Optional[str]:
|
51
55
|
local_file = self._local.get_cache_file_path(key)
|
52
56
|
if os.path.exists(local_file):
|
@@ -79,3 +83,18 @@ class GithubFileCache(FileCache):
|
|
79
83
|
await self._client.upload_file(key, cache_file_path)
|
80
84
|
|
81
85
|
return cache_file_path
|
86
|
+
|
87
|
+
#
|
88
|
+
|
89
|
+
async def get_data(self, key: str) -> ta.Optional[DataCache.Data]:
|
90
|
+
local_file = self._local.get_cache_file_path(key)
|
91
|
+
if os.path.exists(local_file):
|
92
|
+
return DataCache.FileData(local_file)
|
93
|
+
|
94
|
+
if (entry := await self._client.get_entry(key)) is None:
|
95
|
+
return None
|
96
|
+
|
97
|
+
return DataCache.UrlData(check.non_empty_str(self._client.get_entry_url(entry)))
|
98
|
+
|
99
|
+
async def put_data(self, key: str, data: DataCache.Data) -> None:
|
100
|
+
await FileCacheDataCache(self).put_data(key, data)
|
omdev/ci/github/client.py
CHANGED
@@ -13,9 +13,9 @@ from omlish.asyncs.asyncio.asyncio import asyncio_wait_concurrent
|
|
13
13
|
from omlish.lite.check import check
|
14
14
|
from omlish.lite.json import json_dumps_compact
|
15
15
|
from omlish.lite.logs import log
|
16
|
+
from omlish.lite.timing import log_timing_context
|
16
17
|
|
17
18
|
from ..consts import CI_CACHE_VERSION
|
18
|
-
from ..utils import log_timing_context
|
19
19
|
from .api import GithubCacheServiceV1
|
20
20
|
from .env import register_github_env_var
|
21
21
|
|
@@ -31,6 +31,9 @@ class GithubCacheClient(abc.ABC):
|
|
31
31
|
def get_entry(self, key: str) -> ta.Awaitable[ta.Optional[Entry]]:
|
32
32
|
raise NotImplementedError
|
33
33
|
|
34
|
+
def get_entry_url(self, entry: Entry) -> ta.Optional[str]:
|
35
|
+
return None
|
36
|
+
|
34
37
|
@abc.abstractmethod
|
35
38
|
def download_file(self, entry: Entry, out_file: str) -> ta.Awaitable[None]:
|
36
39
|
raise NotImplementedError
|
@@ -97,7 +100,7 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
97
100
|
def _get_loop(self) -> asyncio.AbstractEventLoop:
|
98
101
|
if (loop := self._given_loop) is not None:
|
99
102
|
return loop
|
100
|
-
return asyncio.
|
103
|
+
return asyncio.get_running_loop()
|
101
104
|
|
102
105
|
#
|
103
106
|
|
@@ -225,6 +228,10 @@ class GithubCacheServiceV1BaseClient(GithubCacheClient, abc.ABC):
|
|
225
228
|
class Entry(GithubCacheClient.Entry):
|
226
229
|
artifact: GithubCacheServiceV1.ArtifactCacheEntry
|
227
230
|
|
231
|
+
def get_entry_url(self, entry: GithubCacheClient.Entry) -> ta.Optional[str]:
|
232
|
+
entry1 = check.isinstance(entry, self.Entry)
|
233
|
+
return entry1.artifact.cache_key
|
234
|
+
|
228
235
|
#
|
229
236
|
|
230
237
|
def build_get_entry_url_path(self, *keys: str) -> str:
|
omdev/ci/github/inject.py
CHANGED
@@ -6,7 +6,7 @@ from omlish.lite.inject import InjectorBindings
|
|
6
6
|
from omlish.lite.inject import inj
|
7
7
|
|
8
8
|
from ..cache import FileCache
|
9
|
-
from .cache import
|
9
|
+
from .cache import GithubCache
|
10
10
|
|
11
11
|
|
12
12
|
##
|
@@ -20,11 +20,11 @@ def bind_github(
|
|
20
20
|
|
21
21
|
if cache_dir is not None:
|
22
22
|
lst.extend([
|
23
|
-
inj.bind(
|
23
|
+
inj.bind(GithubCache.Config(
|
24
24
|
dir=cache_dir,
|
25
25
|
)),
|
26
|
-
inj.bind(
|
27
|
-
inj.bind(FileCache, to_key=
|
26
|
+
inj.bind(GithubCache, singleton=True),
|
27
|
+
inj.bind(FileCache, to_key=GithubCache),
|
28
28
|
])
|
29
29
|
|
30
30
|
return inj.as_bindings(*lst)
|
omdev/ci/requirements.py
CHANGED
omdev/ci/utils.py
CHANGED
@@ -1,11 +1,7 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import hashlib
|
3
|
-
import logging
|
4
|
-
import time
|
5
3
|
import typing as ta
|
6
4
|
|
7
|
-
from omlish.lite.logs import log
|
8
|
-
|
9
5
|
|
10
6
|
##
|
11
7
|
|
@@ -22,48 +18,3 @@ def read_yaml_file(yaml_file: str) -> ta.Any:
|
|
22
18
|
|
23
19
|
def sha256_str(s: str) -> str:
|
24
20
|
return hashlib.sha256(s.encode('utf-8')).hexdigest()
|
25
|
-
|
26
|
-
|
27
|
-
##
|
28
|
-
|
29
|
-
|
30
|
-
class LogTimingContext:
|
31
|
-
DEFAULT_LOG: ta.ClassVar[logging.Logger] = log
|
32
|
-
|
33
|
-
def __init__(
|
34
|
-
self,
|
35
|
-
description: str,
|
36
|
-
*,
|
37
|
-
log: ta.Optional[logging.Logger] = None, # noqa
|
38
|
-
level: int = logging.DEBUG,
|
39
|
-
) -> None:
|
40
|
-
super().__init__()
|
41
|
-
|
42
|
-
self._description = description
|
43
|
-
self._log = log if log is not None else self.DEFAULT_LOG
|
44
|
-
self._level = level
|
45
|
-
|
46
|
-
def set_description(self, description: str) -> 'LogTimingContext':
|
47
|
-
self._description = description
|
48
|
-
return self
|
49
|
-
|
50
|
-
_begin_time: float
|
51
|
-
_end_time: float
|
52
|
-
|
53
|
-
def __enter__(self) -> 'LogTimingContext':
|
54
|
-
self._begin_time = time.time()
|
55
|
-
|
56
|
-
self._log.log(self._level, f'Begin : {self._description}') # noqa
|
57
|
-
|
58
|
-
return self
|
59
|
-
|
60
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
61
|
-
self._end_time = time.time()
|
62
|
-
|
63
|
-
self._log.log(
|
64
|
-
self._level,
|
65
|
-
f'End : {self._description} - {self._end_time - self._begin_time:0.2f} s elapsed',
|
66
|
-
)
|
67
|
-
|
68
|
-
|
69
|
-
log_timing_context = LogTimingContext
|
omdev/dataserver/targets.py
CHANGED
@@ -16,6 +16,8 @@ class DataServerTarget(abc.ABC): # noqa
|
|
16
16
|
content_type: ta.Optional[str] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
|
17
17
|
content_length: ta.Optional[int] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True})
|
18
18
|
|
19
|
+
#
|
20
|
+
|
19
21
|
@classmethod
|
20
22
|
def of(
|
21
23
|
cls,
|
@@ -62,6 +64,36 @@ class DataServerTarget(abc.ABC): # noqa
|
|
62
64
|
else:
|
63
65
|
raise TypeError('No target type provided')
|
64
66
|
|
67
|
+
#
|
68
|
+
|
69
|
+
@classmethod
|
70
|
+
def of_bytes(cls, data: bytes) -> 'BytesDataServerTarget':
|
71
|
+
return BytesDataServerTarget(
|
72
|
+
data=data,
|
73
|
+
content_type='application/octet-stream',
|
74
|
+
)
|
75
|
+
|
76
|
+
@classmethod
|
77
|
+
def of_text(cls, data: str) -> 'BytesDataServerTarget':
|
78
|
+
return BytesDataServerTarget(
|
79
|
+
data=data.encode('utf-8'),
|
80
|
+
content_type='text/plain; charset=utf-8',
|
81
|
+
)
|
82
|
+
|
83
|
+
@classmethod
|
84
|
+
def of_json(cls, data: str) -> 'BytesDataServerTarget':
|
85
|
+
return BytesDataServerTarget(
|
86
|
+
data=data.encode('utf-8'),
|
87
|
+
content_type='application/json; charset=utf-8',
|
88
|
+
)
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def of_html(cls, data: str) -> 'BytesDataServerTarget':
|
92
|
+
return BytesDataServerTarget(
|
93
|
+
data=data.encode('utf-8'),
|
94
|
+
content_type='text/html; charset=utf-8',
|
95
|
+
)
|
96
|
+
|
65
97
|
|
66
98
|
@dc.dataclass(frozen=True)
|
67
99
|
class BytesDataServerTarget(DataServerTarget):
|
omdev/git/revisions.py
CHANGED
@@ -4,8 +4,8 @@ import os.path
|
|
4
4
|
import subprocess
|
5
5
|
import typing as ta
|
6
6
|
|
7
|
-
from omlish.subprocesses import
|
8
|
-
from omlish.subprocesses import
|
7
|
+
from omlish.subprocesses.sync import subprocesses
|
8
|
+
from omlish.subprocesses.wrap import subprocess_maybe_shell_wrap_exec
|
9
9
|
|
10
10
|
|
11
11
|
def get_git_revision(
|
omdev/git/shallow.py
CHANGED
omdev/git/status.py
CHANGED
@@ -7,7 +7,7 @@ import subprocess
|
|
7
7
|
import typing as ta
|
8
8
|
|
9
9
|
from omlish.lite.check import check
|
10
|
-
from omlish.subprocesses import subprocess_maybe_shell_wrap_exec
|
10
|
+
from omlish.subprocesses.wrap import subprocess_maybe_shell_wrap_exec
|
11
11
|
|
12
12
|
|
13
13
|
_GIT_STATUS_LINE_ESCAPE_CODES: ta.Mapping[str, str] = {
|
omdev/oci/data.py
CHANGED
@@ -5,6 +5,7 @@ import dataclasses as dc
|
|
5
5
|
import enum
|
6
6
|
import typing as ta
|
7
7
|
|
8
|
+
from omlish.lite.check import check
|
8
9
|
from omlish.lite.marshal import OBJ_MARSHALER_FIELD_KEY
|
9
10
|
from omlish.lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
|
10
11
|
|
@@ -149,3 +150,21 @@ def is_empty_oci_dataclass(obj: OciDataclass) -> bool:
|
|
149
150
|
|
150
151
|
else:
|
151
152
|
return False
|
153
|
+
|
154
|
+
|
155
|
+
##
|
156
|
+
|
157
|
+
|
158
|
+
def get_single_leaf_oci_image_index(image_index: OciImageIndex) -> OciImageIndex:
|
159
|
+
while True:
|
160
|
+
child_manifest = check.single(image_index.manifests)
|
161
|
+
if isinstance(child_manifest, OciImageManifest):
|
162
|
+
break
|
163
|
+
image_index = check.isinstance(child_manifest, OciImageIndex)
|
164
|
+
|
165
|
+
return image_index
|
166
|
+
|
167
|
+
|
168
|
+
def get_single_oci_image_manifest(image_index: OciImageIndex) -> OciImageManifest:
|
169
|
+
child_index = check.single(image_index.manifests)
|
170
|
+
return check.isinstance(child_index, OciImageManifest)
|
omdev/oci/dataserver.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# ruff: noqa:
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
2
|
import typing as ta
|
3
3
|
|
4
4
|
from omlish.lite.check import check
|
@@ -13,6 +13,9 @@ from .datarefs import open_oci_data_ref
|
|
13
13
|
from .media import OCI_MANIFEST_MEDIA_TYPES
|
14
14
|
|
15
15
|
|
16
|
+
##
|
17
|
+
|
18
|
+
|
16
19
|
def build_oci_repository_data_server_routes(
|
17
20
|
repo_name: str,
|
18
21
|
built_repo: BuiltOciImageIndexRepository,
|
File without changes
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import heapq
|
4
|
+
import tarfile
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from omlish.lite.cached import cached_nullary
|
8
|
+
from omlish.lite.check import check
|
9
|
+
from omlish.lite.contextmanagers import ExitStacked
|
10
|
+
|
11
|
+
from ..compression import OciCompression
|
12
|
+
from ..tars import OciDataTarWriter
|
13
|
+
from ..tars import WrittenOciDataTarFileInfo
|
14
|
+
|
15
|
+
|
16
|
+
##
|
17
|
+
|
18
|
+
|
19
|
+
class OciLayerPacker(ExitStacked):
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
input_file_path: str,
|
23
|
+
output_file_paths: ta.Sequence[str],
|
24
|
+
*,
|
25
|
+
compression: ta.Optional[OciCompression] = None,
|
26
|
+
) -> None:
|
27
|
+
super().__init__()
|
28
|
+
|
29
|
+
self._input_file_path = input_file_path
|
30
|
+
self._output_file_paths = list(output_file_paths)
|
31
|
+
self._compression = compression
|
32
|
+
|
33
|
+
self._output_file_indexes_by_name: ta.Dict[str, int] = {}
|
34
|
+
|
35
|
+
#
|
36
|
+
|
37
|
+
@cached_nullary
|
38
|
+
def _input_tar_file(self) -> tarfile.TarFile:
|
39
|
+
# FIXME: check uncompressed
|
40
|
+
return self._enter_context(tarfile.open(self._input_file_path))
|
41
|
+
|
42
|
+
#
|
43
|
+
|
44
|
+
@cached_nullary
|
45
|
+
def _entries_by_name(self) -> ta.Mapping[str, tarfile.TarInfo]:
|
46
|
+
return {
|
47
|
+
info.name: info
|
48
|
+
for info in self._input_tar_file().getmembers()
|
49
|
+
}
|
50
|
+
|
51
|
+
#
|
52
|
+
|
53
|
+
class _CategorizedEntries(ta.NamedTuple):
|
54
|
+
files_by_name: ta.Mapping[str, tarfile.TarInfo]
|
55
|
+
non_files_by_name: ta.Mapping[str, tarfile.TarInfo]
|
56
|
+
links_by_name: ta.Mapping[str, tarfile.TarInfo]
|
57
|
+
|
58
|
+
@cached_nullary
|
59
|
+
def _categorized_entries(self) -> _CategorizedEntries:
|
60
|
+
files_by_name: ta.Dict[str, tarfile.TarInfo] = {}
|
61
|
+
non_files_by_name: ta.Dict[str, tarfile.TarInfo] = {}
|
62
|
+
links_by_name: ta.Dict[str, tarfile.TarInfo] = {}
|
63
|
+
|
64
|
+
for name, info in self._entries_by_name().items():
|
65
|
+
if info.type in tarfile.REGULAR_TYPES:
|
66
|
+
files_by_name[name] = info
|
67
|
+
elif info.type in (tarfile.LNKTYPE, tarfile.GNUTYPE_LONGLINK):
|
68
|
+
links_by_name[name] = info
|
69
|
+
else:
|
70
|
+
non_files_by_name[name] = info
|
71
|
+
|
72
|
+
return self._CategorizedEntries(
|
73
|
+
files_by_name=files_by_name,
|
74
|
+
non_files_by_name=non_files_by_name,
|
75
|
+
links_by_name=links_by_name,
|
76
|
+
)
|
77
|
+
|
78
|
+
#
|
79
|
+
|
80
|
+
@cached_nullary
|
81
|
+
def _non_files_sorted_by_name(self) -> ta.Sequence[tarfile.TarInfo]:
|
82
|
+
return sorted(
|
83
|
+
self._categorized_entries().non_files_by_name.values(),
|
84
|
+
key=lambda info: info.name,
|
85
|
+
)
|
86
|
+
|
87
|
+
@cached_nullary
|
88
|
+
def _files_descending_by_size(self) -> ta.Sequence[tarfile.TarInfo]:
|
89
|
+
return sorted(
|
90
|
+
self._categorized_entries().files_by_name.values(),
|
91
|
+
key=lambda info: -check.isinstance(info.size, int),
|
92
|
+
)
|
93
|
+
|
94
|
+
#
|
95
|
+
|
96
|
+
@cached_nullary
|
97
|
+
def _output_files(self) -> ta.Sequence[ta.BinaryIO]:
|
98
|
+
return [
|
99
|
+
self._enter_context(open(output_file_path, 'wb'))
|
100
|
+
for output_file_path in self._output_file_paths
|
101
|
+
]
|
102
|
+
|
103
|
+
@cached_nullary
|
104
|
+
def _output_tar_writers(self) -> ta.Sequence[OciDataTarWriter]:
|
105
|
+
return [
|
106
|
+
self._enter_context(
|
107
|
+
OciDataTarWriter(
|
108
|
+
output_file,
|
109
|
+
compression=self._compression,
|
110
|
+
),
|
111
|
+
)
|
112
|
+
for output_file in self._output_files()
|
113
|
+
]
|
114
|
+
|
115
|
+
#
|
116
|
+
|
117
|
+
def _write_entry(
|
118
|
+
self,
|
119
|
+
info: tarfile.TarInfo,
|
120
|
+
output_file_idx: int,
|
121
|
+
) -> None:
|
122
|
+
check.not_in(info.name, self._output_file_indexes_by_name)
|
123
|
+
|
124
|
+
writer = self._output_tar_writers()[output_file_idx]
|
125
|
+
|
126
|
+
if info.type in tarfile.REGULAR_TYPES:
|
127
|
+
with check.not_none(self._input_tar_file().extractfile(info)) as f:
|
128
|
+
writer.add_file(info, f) # type: ignore
|
129
|
+
|
130
|
+
else:
|
131
|
+
writer.add_file(info)
|
132
|
+
|
133
|
+
self._output_file_indexes_by_name[info.name] = output_file_idx
|
134
|
+
|
135
|
+
@cached_nullary
|
136
|
+
def _write_non_files(self) -> None:
|
137
|
+
for non_file in self._non_files_sorted_by_name():
|
138
|
+
self._write_entry(non_file, 0)
|
139
|
+
|
140
|
+
@cached_nullary
|
141
|
+
def _write_files(self) -> None:
|
142
|
+
writers = self._output_tar_writers()
|
143
|
+
|
144
|
+
bins = [
|
145
|
+
(writer.info().compressed_sz, i)
|
146
|
+
for i, writer in enumerate(writers)
|
147
|
+
]
|
148
|
+
|
149
|
+
heapq.heapify(bins)
|
150
|
+
|
151
|
+
for file in self._files_descending_by_size():
|
152
|
+
_, bin_index = heapq.heappop(bins)
|
153
|
+
|
154
|
+
writer = writers[bin_index]
|
155
|
+
|
156
|
+
self._write_entry(file, bin_index)
|
157
|
+
|
158
|
+
bin_size = writer.info().compressed_sz
|
159
|
+
|
160
|
+
heapq.heappush(bins, (bin_size, bin_index))
|
161
|
+
|
162
|
+
@cached_nullary
|
163
|
+
def _write_links(self) -> None:
|
164
|
+
for link in self._categorized_entries().links_by_name.values():
|
165
|
+
link_name = check.non_empty_str(link.linkname)
|
166
|
+
|
167
|
+
output_file_idx = self._output_file_indexes_by_name[link_name]
|
168
|
+
|
169
|
+
self._write_entry(link, output_file_idx)
|
170
|
+
|
171
|
+
@cached_nullary
|
172
|
+
def write(self) -> ta.Mapping[str, WrittenOciDataTarFileInfo]:
|
173
|
+
writers = self._output_tar_writers()
|
174
|
+
|
175
|
+
self._write_non_files()
|
176
|
+
self._write_files()
|
177
|
+
self._write_links()
|
178
|
+
|
179
|
+
for output_tar_writer in writers:
|
180
|
+
output_tar_writer.tar_file().close()
|
181
|
+
|
182
|
+
return {
|
183
|
+
output_file_path: output_tar_writer.info()
|
184
|
+
for output_file_path, output_tar_writer in zip(self._output_file_paths, writers)
|
185
|
+
}
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# @omlish-lite
|
3
|
+
import copy
|
4
|
+
import os.path
|
5
|
+
import shutil
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from omlish.lite.cached import cached_nullary
|
9
|
+
from omlish.lite.check import check
|
10
|
+
from omlish.lite.contextmanagers import ExitStacked
|
11
|
+
from omlish.lite.timing import log_timing_context
|
12
|
+
from omlish.os.temp import temp_dir_context
|
13
|
+
|
14
|
+
from ...oci.tars import WrittenOciDataTarFileInfo
|
15
|
+
from ..building import BuiltOciImageIndexRepository
|
16
|
+
from ..building import build_oci_index_repository
|
17
|
+
from ..compression import OciCompression
|
18
|
+
from ..data import OciImageIndex
|
19
|
+
from ..data import OciImageLayer
|
20
|
+
from ..data import OciImageManifest
|
21
|
+
from ..data import get_single_leaf_oci_image_index
|
22
|
+
from ..data import get_single_oci_image_manifest
|
23
|
+
from ..datarefs import FileOciDataRef
|
24
|
+
from ..datarefs import open_oci_data_ref
|
25
|
+
from ..loading import read_oci_repository_root_index
|
26
|
+
from ..repositories import OciRepository
|
27
|
+
from .packing import OciLayerPacker
|
28
|
+
from .unpacking import OciLayerUnpacker
|
29
|
+
|
30
|
+
|
31
|
+
##
|
32
|
+
|
33
|
+
|
34
|
+
class OciPackedRepositoryBuilder(ExitStacked):
|
35
|
+
def __init__(
|
36
|
+
self,
|
37
|
+
source_repo: OciRepository,
|
38
|
+
*,
|
39
|
+
temp_dir: ta.Optional[str] = None,
|
40
|
+
|
41
|
+
num_packed_files: int = 3, # GH actions have this set to 3, the default
|
42
|
+
packed_compression: ta.Optional[OciCompression] = OciCompression.ZSTD,
|
43
|
+
) -> None:
|
44
|
+
super().__init__()
|
45
|
+
|
46
|
+
self._source_repo = source_repo
|
47
|
+
|
48
|
+
self._given_temp_dir = temp_dir
|
49
|
+
|
50
|
+
check.arg(num_packed_files > 0)
|
51
|
+
self._num_packed_files = num_packed_files
|
52
|
+
|
53
|
+
self._packed_compression = packed_compression
|
54
|
+
|
55
|
+
@cached_nullary
|
56
|
+
def _temp_dir(self) -> str:
|
57
|
+
if (given := self._given_temp_dir) is not None:
|
58
|
+
return given
|
59
|
+
else:
|
60
|
+
return self._enter_context(temp_dir_context()) # noqa
|
61
|
+
|
62
|
+
#
|
63
|
+
|
64
|
+
@cached_nullary
|
65
|
+
def _source_image_index(self) -> OciImageIndex:
|
66
|
+
image_index = read_oci_repository_root_index(self._source_repo)
|
67
|
+
return get_single_leaf_oci_image_index(image_index)
|
68
|
+
|
69
|
+
@cached_nullary
|
70
|
+
def _source_image_manifest(self) -> OciImageManifest:
|
71
|
+
return get_single_oci_image_manifest(self._source_image_index())
|
72
|
+
|
73
|
+
#
|
74
|
+
|
75
|
+
@cached_nullary
|
76
|
+
def _extracted_layer_tar_files(self) -> ta.List[str]:
|
77
|
+
image = self._source_image_manifest()
|
78
|
+
|
79
|
+
layer_tar_files = []
|
80
|
+
|
81
|
+
for i, layer in enumerate(image.layers):
|
82
|
+
if isinstance(layer.data, FileOciDataRef):
|
83
|
+
input_file_path = layer.data.path
|
84
|
+
|
85
|
+
else:
|
86
|
+
input_file_path = os.path.join(self._temp_dir(), f'save-layer-{i}.tar')
|
87
|
+
with open(input_file_path, 'wb') as input_file: # noqa
|
88
|
+
with open_oci_data_ref(layer.data) as layer_file:
|
89
|
+
shutil.copyfileobj(layer_file, input_file, length=1024 * 1024) # noqa
|
90
|
+
|
91
|
+
layer_tar_files.append(input_file_path)
|
92
|
+
|
93
|
+
return layer_tar_files
|
94
|
+
|
95
|
+
#
|
96
|
+
|
97
|
+
@cached_nullary
|
98
|
+
def _unpacked_tar_file(self) -> str:
|
99
|
+
layer_tar_files = self._extracted_layer_tar_files()
|
100
|
+
unpacked_file = os.path.join(self._temp_dir(), 'unpacked.tar')
|
101
|
+
|
102
|
+
with log_timing_context(f'Unpacking docker image {self._source_repo}'):
|
103
|
+
with OciLayerUnpacker(
|
104
|
+
layer_tar_files,
|
105
|
+
unpacked_file,
|
106
|
+
) as lu:
|
107
|
+
lu.write()
|
108
|
+
|
109
|
+
return unpacked_file
|
110
|
+
|
111
|
+
#
|
112
|
+
|
113
|
+
@cached_nullary
|
114
|
+
def _packed_tar_files(self) -> ta.Mapping[str, WrittenOciDataTarFileInfo]:
|
115
|
+
unpacked_tar_file = self._unpacked_tar_file()
|
116
|
+
|
117
|
+
packed_tar_files = [
|
118
|
+
os.path.join(self._temp_dir(), f'packed-{i}.tar')
|
119
|
+
for i in range(self._num_packed_files)
|
120
|
+
]
|
121
|
+
|
122
|
+
with log_timing_context(f'Packing docker image {self._source_repo}'):
|
123
|
+
with OciLayerPacker(
|
124
|
+
unpacked_tar_file,
|
125
|
+
packed_tar_files,
|
126
|
+
compression=self._packed_compression,
|
127
|
+
) as lp:
|
128
|
+
return lp.write()
|
129
|
+
|
130
|
+
#
|
131
|
+
|
132
|
+
@cached_nullary
|
133
|
+
def _packed_image_index(self) -> OciImageIndex:
|
134
|
+
image_index = copy.deepcopy(self._source_image_index())
|
135
|
+
|
136
|
+
image = get_single_oci_image_manifest(image_index)
|
137
|
+
|
138
|
+
image.config.history = None
|
139
|
+
|
140
|
+
written = self._packed_tar_files()
|
141
|
+
|
142
|
+
# FIXME: use prebuilt sha256
|
143
|
+
image.layers = [
|
144
|
+
OciImageLayer(
|
145
|
+
kind=OciImageLayer.Kind.from_compression(self._packed_compression),
|
146
|
+
data=FileOciDataRef(output_file),
|
147
|
+
)
|
148
|
+
for output_file, output_file_info in written.items()
|
149
|
+
]
|
150
|
+
|
151
|
+
image.config.rootfs.diff_ids = [
|
152
|
+
f'sha256:{output_file_info.tar_sha256}'
|
153
|
+
for output_file_info in written.values()
|
154
|
+
]
|
155
|
+
|
156
|
+
return image_index
|
157
|
+
|
158
|
+
#
|
159
|
+
|
160
|
+
@cached_nullary
|
161
|
+
def build(self) -> BuiltOciImageIndexRepository:
|
162
|
+
return build_oci_index_repository(self._packed_image_index())
|