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.
@@ -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 GithubFileCache(FileCache):
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.get_event_loop()
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 GithubFileCache
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(GithubFileCache.Config(
23
+ inj.bind(GithubCache.Config(
24
24
  dir=cache_dir,
25
25
  )),
26
- inj.bind(GithubFileCache, singleton=True),
27
- inj.bind(FileCache, to_key=GithubFileCache),
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
@@ -18,7 +18,7 @@ import typing as ta
18
18
 
19
19
  from omlish.lite.check import check
20
20
  from omlish.lite.contextmanagers import defer
21
- from omlish.subprocesses import subprocesses
21
+ from omlish.subprocesses.sync import subprocesses
22
22
 
23
23
  from .utils import sha256_str
24
24
 
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
@@ -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 subprocess_maybe_shell_wrap_exec
8
- from omlish.subprocesses import subprocesses
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
@@ -4,7 +4,7 @@ import dataclasses as dc
4
4
  import os.path
5
5
  import typing as ta
6
6
 
7
- from omlish.subprocesses import subprocesses
7
+ from omlish.subprocesses.sync import subprocesses
8
8
 
9
9
 
10
10
  @dc.dataclass(frozen=True)
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: PT009 UP006 UP007
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())