omdev 0.0.0.dev223__py3-none-any.whl → 0.0.0.dev225__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.
- 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())
|