omdev 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__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.
Files changed (48) hide show
  1. omdev/ci/cache.py +148 -23
  2. omdev/ci/ci.py +50 -110
  3. omdev/ci/cli.py +24 -23
  4. omdev/ci/docker/__init__.py +0 -0
  5. omdev/ci/docker/buildcaching.py +69 -0
  6. omdev/ci/docker/cache.py +57 -0
  7. omdev/ci/docker/cacheserved.py +262 -0
  8. omdev/ci/{docker.py → docker/cmds.py} +1 -44
  9. omdev/ci/docker/dataserver.py +204 -0
  10. omdev/ci/docker/imagepulling.py +65 -0
  11. omdev/ci/docker/inject.py +37 -0
  12. omdev/ci/docker/packing.py +72 -0
  13. omdev/ci/docker/repositories.py +40 -0
  14. omdev/ci/docker/utils.py +48 -0
  15. omdev/ci/github/cache.py +35 -6
  16. omdev/ci/github/client.py +9 -2
  17. omdev/ci/github/inject.py +30 -0
  18. omdev/ci/inject.py +61 -0
  19. omdev/ci/utils.py +0 -49
  20. omdev/dataserver/__init__.py +1 -0
  21. omdev/dataserver/handlers.py +198 -0
  22. omdev/dataserver/http.py +69 -0
  23. omdev/dataserver/routes.py +49 -0
  24. omdev/dataserver/server.py +90 -0
  25. omdev/dataserver/targets.py +121 -0
  26. omdev/oci/building.py +107 -9
  27. omdev/oci/compression.py +8 -0
  28. omdev/oci/data.py +43 -0
  29. omdev/oci/datarefs.py +90 -50
  30. omdev/oci/dataserver.py +64 -0
  31. omdev/oci/loading.py +20 -0
  32. omdev/oci/media.py +20 -0
  33. omdev/oci/pack/__init__.py +0 -0
  34. omdev/oci/pack/packing.py +185 -0
  35. omdev/oci/pack/repositories.py +162 -0
  36. omdev/oci/pack/unpacking.py +204 -0
  37. omdev/oci/repositories.py +84 -2
  38. omdev/oci/tars.py +144 -0
  39. omdev/pyproject/resources/python.sh +1 -1
  40. omdev/scripts/ci.py +2137 -512
  41. omdev/scripts/interp.py +119 -22
  42. omdev/scripts/pyproject.py +141 -28
  43. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/METADATA +2 -2
  44. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/RECORD +48 -23
  45. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/LICENSE +0 -0
  46. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/WHEEL +0 -0
  47. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/entry_points.txt +0 -0
  48. {omdev-0.0.0.dev222.dist-info → omdev-0.0.0.dev224.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,64 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.check import check
5
+
6
+ from ..dataserver.routes import DataServerRoute
7
+ from ..dataserver.targets import DataServerTarget
8
+ from .building import BuiltOciImageIndexRepository
9
+ from .building import OciRepositoryBuilder
10
+ from .datarefs import BytesOciDataRef
11
+ from .datarefs import FileOciDataRef
12
+ from .datarefs import open_oci_data_ref
13
+ from .media import OCI_MANIFEST_MEDIA_TYPES
14
+
15
+
16
+ ##
17
+
18
+
19
+ def build_oci_repository_data_server_routes(
20
+ repo_name: str,
21
+ built_repo: BuiltOciImageIndexRepository,
22
+ ) -> ta.List[DataServerRoute]:
23
+ base_url_path = f'/v2/{repo_name}'
24
+
25
+ repo_contents: ta.Dict[str, OciRepositoryBuilder.Blob] = {}
26
+
27
+ repo_contents[f'{base_url_path}/manifests/latest'] = built_repo.blobs[built_repo.media_index_descriptor.digest]
28
+
29
+ for blob in built_repo.blobs.values():
30
+ repo_contents['/'.join([
31
+ base_url_path,
32
+ 'manifests' if blob.media_type in OCI_MANIFEST_MEDIA_TYPES else 'blobs',
33
+ blob.digest,
34
+ ])] = blob
35
+
36
+ #
37
+
38
+ def build_blob_target(blob: OciRepositoryBuilder.Blob) -> ta.Optional[DataServerTarget]: # noqa
39
+ kw: dict = dict(
40
+ content_type=check.non_empty_str(blob.media_type),
41
+ )
42
+
43
+ if isinstance(blob.data, BytesOciDataRef):
44
+ return DataServerTarget.of(blob.data.data, **kw)
45
+
46
+ elif isinstance(blob.data, FileOciDataRef):
47
+ return DataServerTarget.of(file_path=blob.data.path, **kw)
48
+
49
+ else:
50
+ with open_oci_data_ref(blob.data) as f:
51
+ data = f.read()
52
+
53
+ return DataServerTarget.of(data, **kw)
54
+
55
+ #
56
+
57
+ return [
58
+ DataServerRoute(
59
+ paths=[path],
60
+ target=target,
61
+ )
62
+ for path, blob in repo_contents.items()
63
+ if (target := build_blob_target(blob)) is not None
64
+ ]
omdev/oci/loading.py CHANGED
@@ -18,6 +18,7 @@ from .media import OciMediaImageConfig
18
18
  from .media import OciMediaImageIndex
19
19
  from .media import OciMediaImageManifest
20
20
  from .media import unmarshal_oci_media_dataclass
21
+ from .repositories import FileOciRepository
21
22
  from .repositories import OciRepository
22
23
 
23
24
 
@@ -120,3 +121,22 @@ class OciRepositoryLoader:
120
121
 
121
122
  else:
122
123
  raise TypeError(obj)
124
+
125
+
126
+ ##
127
+
128
+
129
+ def read_oci_repository_root_index(
130
+ obj: ta.Any,
131
+ *,
132
+ file_name: str = 'index.json',
133
+ ) -> OciImageIndex:
134
+ file_repo = check.isinstance(OciRepository.of(obj), FileOciRepository)
135
+
136
+ repo_ldr = OciRepositoryLoader(file_repo)
137
+
138
+ media_image_idx = repo_ldr.load_object(file_repo.read_file(file_name), OciMediaImageIndex)
139
+
140
+ image_idx = repo_ldr.from_media(media_image_idx)
141
+
142
+ return check.isinstance(image_idx, OciImageIndex)
omdev/oci/media.py CHANGED
@@ -25,8 +25,19 @@ OCI_MEDIA_FIELDS: ta.Collection[str] = frozenset([
25
25
  @dc.dataclass()
26
26
  class OciMediaDataclass(abc.ABC): # noqa
27
27
  SCHEMA_VERSION: ta.ClassVar[int]
28
+
29
+ @property
30
+ def schema_version(self) -> int:
31
+ raise TypeError
32
+
28
33
  MEDIA_TYPE: ta.ClassVar[str]
29
34
 
35
+ @property
36
+ def media_type(self) -> str:
37
+ raise TypeError
38
+
39
+ #
40
+
30
41
  def __init_subclass__(cls, **kwargs: ta.Any) -> None:
31
42
  super().__init_subclass__(**kwargs)
32
43
  for a in OCI_MEDIA_FIELDS:
@@ -157,3 +168,12 @@ class OciMediaImageConfig(OciImageConfig, OciMediaDataclass):
157
168
 
158
169
  MEDIA_TYPE: ta.ClassVar[str] = 'application/vnd.oci.image.config.v1+json'
159
170
  media_type: str = dc.field(default=MEDIA_TYPE, metadata={OBJ_MARSHALER_FIELD_KEY: 'mediaType'})
171
+
172
+
173
+ ##
174
+
175
+
176
+ OCI_MANIFEST_MEDIA_TYPES: ta.AbstractSet[str] = frozenset([
177
+ OciMediaImageIndex.MEDIA_TYPE,
178
+ OciMediaImageManifest.MEDIA_TYPE,
179
+ ])
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())
@@ -0,0 +1,204 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import contextlib
4
+ import os.path
5
+ import tarfile
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
+
12
+
13
+ ##
14
+
15
+
16
+ class OciLayerUnpacker(ExitStacked):
17
+ def __init__(
18
+ self,
19
+ input_files: ta.Sequence[ta.Union[str, tarfile.TarFile]],
20
+ output_file_path: str,
21
+ ) -> None:
22
+ super().__init__()
23
+
24
+ self._input_files = list(input_files)
25
+ self._output_file_path = output_file_path
26
+
27
+ #
28
+
29
+ @contextlib.contextmanager
30
+ def _open_input_file(self, input_file: ta.Union[str, tarfile.TarFile]) -> ta.Iterator[tarfile.TarFile]:
31
+ if isinstance(input_file, tarfile.TarFile):
32
+ yield input_file
33
+
34
+ elif isinstance(input_file, str):
35
+ with tarfile.open(input_file) as tar_file:
36
+ yield tar_file
37
+
38
+ else:
39
+ raise TypeError(input_file)
40
+
41
+ #
42
+
43
+ class _Entry(ta.NamedTuple):
44
+ file: ta.Union[str, tarfile.TarFile]
45
+ info: tarfile.TarInfo
46
+
47
+ def _build_input_file_sorted_entries(self, input_file: ta.Union[str, tarfile.TarFile]) -> ta.Sequence[_Entry]:
48
+ dct: ta.Dict[str, OciLayerUnpacker._Entry] = {}
49
+
50
+ with self._open_input_file(input_file) as input_tar_file:
51
+ for info in input_tar_file.getmembers():
52
+ check.not_in(info.name, dct)
53
+ dct[info.name] = self._Entry(
54
+ file=input_file,
55
+ info=info,
56
+ )
57
+
58
+ return sorted(dct.values(), key=lambda entry: entry.info.name)
59
+
60
+ @cached_nullary
61
+ def _entries_by_name(self) -> ta.Mapping[str, _Entry]:
62
+ root: dict = {}
63
+
64
+ def find_dir(dir_name: str) -> dict: # noqa
65
+ if dir_name:
66
+ dir_parts = dir_name.split('/')
67
+ else:
68
+ dir_parts = []
69
+
70
+ cur = root # noqa
71
+ for dir_part in dir_parts:
72
+ cur = cur[dir_part] # noqa
73
+
74
+ return check.isinstance(cur, dict)
75
+
76
+ #
77
+
78
+ for input_file in self._input_files:
79
+ sorted_entries = self._build_input_file_sorted_entries(input_file)
80
+
81
+ wh_names = set()
82
+ wh_opaques = set()
83
+
84
+ #
85
+
86
+ for entry in sorted_entries:
87
+ info = entry.info
88
+ name = check.non_empty_str(info.name)
89
+ base_name = os.path.basename(name)
90
+ dir_name = os.path.dirname(name)
91
+
92
+ if base_name == '.wh..wh..opq':
93
+ wh_opaques.add(dir_name)
94
+ continue
95
+
96
+ if base_name.startswith('.wh.'):
97
+ wh_base_name = os.path.basename(base_name[4:])
98
+ wh_name = os.path.join(dir_name, wh_base_name)
99
+ wh_names.add(wh_name)
100
+ continue
101
+
102
+ cur = find_dir(dir_name)
103
+
104
+ if info.type == tarfile.DIRTYPE:
105
+ try:
106
+ ex = cur[base_name]
107
+ except KeyError:
108
+ cur[base_name] = {'': entry}
109
+ else:
110
+ ex[''] = entry
111
+
112
+ else:
113
+ cur[base_name] = entry
114
+
115
+ #
116
+
117
+ for wh_name in reversed(sorted(wh_names)): # noqa
118
+ wh_dir_name = os.path.dirname(wh_name)
119
+ wh_base_name = os.path.basename(wh_name)
120
+
121
+ cur = find_dir(wh_dir_name)
122
+ rm = cur[wh_base_name]
123
+
124
+ if isinstance(rm, dict):
125
+ # Whiteouts wipe out whole directory:
126
+ # https://github.com/containerd/containerd/blob/59c8cf6ea5f4175ad512914dd5ce554942bf144f/pkg/archive/tar_test.go#L648
127
+ # check.equal(set(rm), '')
128
+ del cur[wh_base_name]
129
+
130
+ elif isinstance(rm, self._Entry):
131
+ del cur[wh_base_name]
132
+
133
+ else:
134
+ raise TypeError(rm)
135
+
136
+ if wh_opaques:
137
+ raise NotImplementedError
138
+
139
+ #
140
+
141
+ out: ta.Dict[str, OciLayerUnpacker._Entry] = {}
142
+
143
+ def rec(cur): # noqa
144
+ for _, child in sorted(cur.items(), key=lambda t: t[0]):
145
+ if isinstance(child, dict):
146
+ rec(child)
147
+
148
+ elif isinstance(child, self._Entry):
149
+ check.not_in(child.info.name, out)
150
+ out[child.info.name] = child
151
+
152
+ else:
153
+ raise TypeError(child)
154
+
155
+ rec(root)
156
+
157
+ return out
158
+
159
+ #
160
+
161
+ @cached_nullary
162
+ def _output_tar_file(self) -> tarfile.TarFile:
163
+ return self._enter_context(tarfile.open(self._output_file_path, 'w'))
164
+
165
+ #
166
+
167
+ def _add_unpacked_entry(
168
+ self,
169
+ input_tar_file: tarfile.TarFile,
170
+ info: tarfile.TarInfo,
171
+ ) -> None:
172
+ base_name = os.path.basename(info.name)
173
+ check.state(not base_name.startswith('.wh.'))
174
+
175
+ if info.type in tarfile.REGULAR_TYPES:
176
+ with check.not_none(input_tar_file.extractfile(info)) as f:
177
+ self._output_tar_file().addfile(info, f)
178
+
179
+ else:
180
+ self._output_tar_file().addfile(info)
181
+
182
+ def _unpack_file(
183
+ self,
184
+ input_file: ta.Union[str, tarfile.TarFile],
185
+ ) -> None:
186
+ entries_by_name = self._entries_by_name()
187
+
188
+ with self._open_input_file(input_file) as input_tar_file:
189
+ info: tarfile.TarInfo
190
+ for info in input_tar_file.getmembers():
191
+ try:
192
+ entry = entries_by_name[info.name]
193
+ except KeyError:
194
+ continue
195
+
196
+ if entry.file != input_file:
197
+ continue
198
+
199
+ self._add_unpacked_entry(input_tar_file, info)
200
+
201
+ @cached_nullary
202
+ def write(self) -> None:
203
+ for input_file in self._input_files:
204
+ self._unpack_file(input_file)