omdev 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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)