omdev 0.0.0.dev209__py3-none-any.whl → 0.0.0.dev211__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
omdev/.manifests.json CHANGED
@@ -357,7 +357,7 @@
357
357
  "module": ".tools.pawk.pawk",
358
358
  "attr": "_CLI_MODULE",
359
359
  "file": "omdev/tools/pawk/pawk.py",
360
- "line": 390,
360
+ "line": 389,
361
361
  "value": {
362
362
  "$.cli.types.CliModule": {
363
363
  "cmd_name": "pawk",
omdev/amalg/main.py CHANGED
@@ -28,6 +28,7 @@ Targets:
28
28
  import argparse
29
29
  import logging
30
30
  import os.path
31
+ import stat
31
32
  import typing as ta
32
33
 
33
34
  from omlish import check
@@ -60,7 +61,15 @@ def _gen_one(
60
61
  if output_path is not None:
61
62
  with open(output_path, 'w') as f:
62
63
  f.write(src)
63
- os.chmod(output_path, os.stat(input_path).st_mode)
64
+
65
+ src_mode = os.stat(input_path).st_mode
66
+ out_mode = (
67
+ src_mode
68
+ | (stat.S_IXUSR if src_mode & stat.S_IRUSR else 0)
69
+ | (stat.S_IXGRP if src_mode & stat.S_IRGRP else 0)
70
+ | (stat.S_IXOTH if src_mode & stat.S_IROTH else 0)
71
+ )
72
+ os.chmod(output_path, out_mode)
64
73
 
65
74
  else:
66
75
  print(src)
omdev/cc/cdeps.py ADDED
@@ -0,0 +1,38 @@
1
+ import dataclasses as dc
2
+ import tomllib
3
+ import typing as ta
4
+
5
+ from omlish import cached
6
+ from omlish import lang
7
+ from omlish import marshal as msh
8
+
9
+
10
+ @dc.dataclass(frozen=True)
11
+ class Cdep:
12
+ @dc.dataclass(frozen=True)
13
+ class Git:
14
+ url: str
15
+ rev: str
16
+
17
+ subtrees: ta.Sequence[str] | None = None
18
+
19
+ git: Git
20
+
21
+ #
22
+
23
+ include: ta.Sequence[str] | None = None
24
+
25
+ #
26
+
27
+ @dc.dataclass(frozen=True)
28
+ class Cmake:
29
+ fetch_content_url: str | None = None
30
+
31
+ cmake: Cmake | None = None
32
+
33
+
34
+ @cached.function
35
+ def load_cdeps() -> ta.Mapping[str, Cdep]:
36
+ src = lang.get_relative_resources(globals=globals())['cdeps.toml'].read_text()
37
+ dct = tomllib.loads(src)
38
+ return msh.unmarshal(dct.get('deps', {}), ta.Mapping[str, Cdep]) # type: ignore
omdev/cc/cdeps.toml CHANGED
@@ -3,9 +3,12 @@ include = ['single_include']
3
3
 
4
4
  [deps.json.git]
5
5
  url = 'https://github.com/nlohmann/json'
6
- rev = '2d42229f4d68c6f86f37468b84ac65e86b815bbb'
6
+ rev = '9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03'
7
7
  subtrees = ['single_include']
8
8
 
9
+ [deps.json.cmake]
10
+ fetch_content_url = 'https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz'
11
+
9
12
  #
10
13
 
11
14
  [deps.pybind11]
omdev/cc/cli.py CHANGED
@@ -19,45 +19,22 @@ TODO:
19
19
  - cext interop
20
20
  - gen cmake
21
21
  """
22
- import dataclasses as dc
23
22
  import os
24
23
  import shlex
25
24
  import shutil
26
25
  import subprocess
27
26
  import tempfile
28
- import tomllib
29
27
  import typing as ta
30
28
 
31
- from omlish import cached
32
29
  from omlish import check
33
- from omlish import lang
34
30
  from omlish import marshal as msh
35
31
  from omlish.argparse import all as ap
36
32
  from omlish.formats import json
37
33
 
38
34
  from .. import magic
39
35
  from ..cache import data as dcache
40
-
41
-
42
- @dc.dataclass(frozen=True)
43
- class Cdep:
44
- @dc.dataclass(frozen=True)
45
- class Git:
46
- url: str
47
- rev: str
48
-
49
- subtrees: ta.Sequence[str] | None = None
50
-
51
- git: Git
52
-
53
- include: ta.Sequence[str] | None = None
54
-
55
-
56
- @cached.function
57
- def load_cdeps() -> ta.Mapping[str, Cdep]:
58
- src = lang.get_relative_resources(globals=globals())['cdeps.toml'].read_text()
59
- dct = tomllib.loads(src)
60
- return msh.unmarshal(dct.get('deps', {}), ta.Mapping[str, Cdep]) # type: ignore
36
+ from .cdeps import Cdep
37
+ from .cdeps import load_cdeps
61
38
 
62
39
 
63
40
  class Cli(ap.Cli):
omdev/ci/__init__.py ADDED
File without changes
omdev/ci/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ if __name__ == '__main__':
2
+ from .cli import _main # noqa
3
+
4
+ _main()
omdev/ci/cache.py ADDED
@@ -0,0 +1,168 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import os.path
5
+ import shlex
6
+ import shutil
7
+ import typing as ta
8
+
9
+ from .shell import ShellCmd
10
+
11
+
12
+ ##
13
+
14
+
15
+ @abc.abstractmethod
16
+ class FileCache(abc.ABC):
17
+ @abc.abstractmethod
18
+ def get_file(self, key: str) -> ta.Optional[str]:
19
+ raise NotImplementedError
20
+
21
+ @abc.abstractmethod
22
+ def put_file(self, key: str, file_path: str) -> ta.Optional[str]:
23
+ raise NotImplementedError
24
+
25
+
26
+ #
27
+
28
+
29
+ class DirectoryFileCache(FileCache):
30
+ def __init__(self, dir: str) -> None: # noqa
31
+ super().__init__()
32
+
33
+ self._dir = dir
34
+
35
+ #
36
+
37
+ def get_cache_file_path(
38
+ self,
39
+ key: str,
40
+ *,
41
+ make_dirs: bool = False,
42
+ ) -> str:
43
+ if make_dirs:
44
+ os.makedirs(self._dir, exist_ok=True)
45
+ return os.path.join(self._dir, key)
46
+
47
+ def format_incomplete_file(self, f: str) -> str:
48
+ return os.path.join(os.path.dirname(f), f'_{os.path.basename(f)}.incomplete')
49
+
50
+ #
51
+
52
+ def get_file(self, key: str) -> ta.Optional[str]:
53
+ cache_file_path = self.get_cache_file_path(key)
54
+ if not os.path.exists(cache_file_path):
55
+ return None
56
+ return cache_file_path
57
+
58
+ def put_file(self, key: str, file_path: str) -> None:
59
+ cache_file_path = self.get_cache_file_path(key, make_dirs=True)
60
+ shutil.copyfile(file_path, cache_file_path)
61
+
62
+
63
+ ##
64
+
65
+
66
+ class ShellCache(abc.ABC):
67
+ @abc.abstractmethod
68
+ def get_file_cmd(self, key: str) -> ta.Optional[ShellCmd]:
69
+ raise NotImplementedError
70
+
71
+ class PutFileCmdContext(abc.ABC):
72
+ def __init__(self) -> None:
73
+ super().__init__()
74
+
75
+ self._state: ta.Literal['open', 'committed', 'aborted'] = 'open'
76
+
77
+ @property
78
+ def state(self) -> ta.Literal['open', 'committed', 'aborted']:
79
+ return self._state
80
+
81
+ #
82
+
83
+ @property
84
+ @abc.abstractmethod
85
+ def cmd(self) -> ShellCmd:
86
+ raise NotImplementedError
87
+
88
+ #
89
+
90
+ def __enter__(self):
91
+ return self
92
+
93
+ def __exit__(self, exc_type, exc_val, exc_tb):
94
+ if exc_val is None:
95
+ self.commit()
96
+ else:
97
+ self.abort()
98
+
99
+ #
100
+
101
+ @abc.abstractmethod
102
+ def _commit(self) -> None:
103
+ raise NotImplementedError
104
+
105
+ def commit(self) -> None:
106
+ if self._state == 'committed':
107
+ return
108
+ elif self._state == 'open':
109
+ self._commit()
110
+ self._state = 'committed'
111
+ else:
112
+ raise RuntimeError(self._state)
113
+
114
+ #
115
+
116
+ @abc.abstractmethod
117
+ def _abort(self) -> None:
118
+ raise NotImplementedError
119
+
120
+ def abort(self) -> None:
121
+ if self._state == 'aborted':
122
+ return
123
+ elif self._state == 'open':
124
+ self._abort()
125
+ self._state = 'committed'
126
+ else:
127
+ raise RuntimeError(self._state)
128
+
129
+ @abc.abstractmethod
130
+ def put_file_cmd(self, key: str) -> PutFileCmdContext:
131
+ raise NotImplementedError
132
+
133
+
134
+ #
135
+
136
+
137
+ class DirectoryShellCache(ShellCache):
138
+ def __init__(self, dfc: DirectoryFileCache) -> None:
139
+ super().__init__()
140
+
141
+ self._dfc = dfc
142
+
143
+ def get_file_cmd(self, key: str) -> ta.Optional[ShellCmd]:
144
+ f = self._dfc.get_file(key)
145
+ if f is None:
146
+ return None
147
+ return ShellCmd(f'cat {shlex.quote(f)}')
148
+
149
+ class _PutFileCmdContext(ShellCache.PutFileCmdContext): # noqa
150
+ def __init__(self, tf: str, f: str) -> None:
151
+ super().__init__()
152
+
153
+ self._tf = tf
154
+ self._f = f
155
+
156
+ @property
157
+ def cmd(self) -> ShellCmd:
158
+ return ShellCmd(f'cat > {shlex.quote(self._tf)}')
159
+
160
+ def _commit(self) -> None:
161
+ os.replace(self._tf, self._f)
162
+
163
+ def _abort(self) -> None:
164
+ os.unlink(self._tf)
165
+
166
+ def put_file_cmd(self, key: str) -> ShellCache.PutFileCmdContext:
167
+ f = self._dfc.get_cache_file_path(key, make_dirs=True)
168
+ return self._PutFileCmdContext(self._dfc.format_incomplete_file(f), f)
omdev/ci/ci.py ADDED
@@ -0,0 +1,249 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+ import os.path
5
+ import shutil
6
+ import tarfile
7
+ import tempfile
8
+ import typing as ta
9
+
10
+ from omlish.lite.cached import cached_nullary
11
+ from omlish.lite.check import check
12
+ from omlish.lite.contextmanagers import ExitStacked
13
+ from omlish.lite.contextmanagers import defer
14
+
15
+ from .cache import FileCache
16
+ from .cache import ShellCache
17
+ from .compose import DockerComposeRun
18
+ from .compose import get_compose_service_dependencies
19
+ from .docker import build_docker_file_hash
20
+ from .docker import build_docker_image
21
+ from .docker import is_docker_image_present
22
+ from .docker import load_docker_tar_cmd
23
+ from .docker import pull_docker_image
24
+ from .docker import save_docker_tar_cmd
25
+ from .requirements import build_requirements_hash
26
+ from .requirements import download_requirements
27
+ from .shell import ShellCmd
28
+ from .utils import log_timing_context
29
+
30
+
31
+ class Ci(ExitStacked):
32
+ FILE_NAME_HASH_LEN = 16
33
+
34
+ @dc.dataclass(frozen=True)
35
+ class Config:
36
+ project_dir: str
37
+
38
+ docker_file: str
39
+
40
+ compose_file: str
41
+ service: str
42
+
43
+ cmd: ShellCmd
44
+
45
+ requirements_txts: ta.Optional[ta.Sequence[str]] = None
46
+
47
+ always_pull: bool = False
48
+
49
+ def __post_init__(self) -> None:
50
+ check.not_isinstance(self.requirements_txts, str)
51
+
52
+ def __init__(
53
+ self,
54
+ cfg: Config,
55
+ *,
56
+ shell_cache: ta.Optional[ShellCache] = None,
57
+ file_cache: ta.Optional[FileCache] = None,
58
+ ) -> None:
59
+ super().__init__()
60
+
61
+ self._cfg = cfg
62
+ self._shell_cache = shell_cache
63
+ self._file_cache = file_cache
64
+
65
+ #
66
+
67
+ def _load_cache_docker_image(self, key: str) -> ta.Optional[str]:
68
+ if self._shell_cache is None:
69
+ return None
70
+
71
+ get_cache_cmd = self._shell_cache.get_file_cmd(key)
72
+ if get_cache_cmd is None:
73
+ return None
74
+
75
+ get_cache_cmd = dc.replace(get_cache_cmd, s=f'{get_cache_cmd.s} | zstd -cd --long') # noqa
76
+
77
+ return load_docker_tar_cmd(get_cache_cmd)
78
+
79
+ def _save_cache_docker_image(self, key: str, image: str) -> None:
80
+ if self._shell_cache is None:
81
+ return
82
+
83
+ with self._shell_cache.put_file_cmd(key) as put_cache:
84
+ put_cache_cmd = put_cache.cmd
85
+
86
+ put_cache_cmd = dc.replace(put_cache_cmd, s=f'zstd | {put_cache_cmd.s}')
87
+
88
+ save_docker_tar_cmd(image, put_cache_cmd)
89
+
90
+ #
91
+
92
+ def _load_docker_image(self, image: str) -> None:
93
+ if not self._cfg.always_pull and is_docker_image_present(image):
94
+ return
95
+
96
+ dep_suffix = image
97
+ for c in '/:.-_':
98
+ dep_suffix = dep_suffix.replace(c, '-')
99
+
100
+ cache_key = f'docker-{dep_suffix}'
101
+ if self._load_cache_docker_image(cache_key) is not None:
102
+ return
103
+
104
+ pull_docker_image(image)
105
+
106
+ self._save_cache_docker_image(cache_key, image)
107
+
108
+ def load_docker_image(self, image: str) -> None:
109
+ with log_timing_context(f'Load docker image: {image}'):
110
+ self._load_docker_image(image)
111
+
112
+ @cached_nullary
113
+ def load_compose_service_dependencies(self) -> None:
114
+ deps = get_compose_service_dependencies(
115
+ self._cfg.compose_file,
116
+ self._cfg.service,
117
+ )
118
+
119
+ for dep_image in deps.values():
120
+ self.load_docker_image(dep_image)
121
+
122
+ #
123
+
124
+ def _resolve_ci_image(self) -> str:
125
+ docker_file_hash = build_docker_file_hash(self._cfg.docker_file)[:self.FILE_NAME_HASH_LEN]
126
+
127
+ cache_key = f'ci-{docker_file_hash}'
128
+ if (cache_image_id := self._load_cache_docker_image(cache_key)) is not None:
129
+ return cache_image_id
130
+
131
+ image_id = build_docker_image(
132
+ self._cfg.docker_file,
133
+ cwd=self._cfg.project_dir,
134
+ )
135
+
136
+ self._save_cache_docker_image(cache_key, image_id)
137
+
138
+ return image_id
139
+
140
+ @cached_nullary
141
+ def resolve_ci_image(self) -> str:
142
+ with log_timing_context('Resolve ci image') as ltc:
143
+ image_id = self._resolve_ci_image()
144
+ ltc.set_description(f'Resolve ci image: {image_id}')
145
+ return image_id
146
+
147
+ #
148
+
149
+ def _resolve_requirements_dir(self) -> str:
150
+ requirements_txts = [
151
+ os.path.join(self._cfg.project_dir, rf)
152
+ for rf in check.not_none(self._cfg.requirements_txts)
153
+ ]
154
+
155
+ requirements_hash = build_requirements_hash(requirements_txts)[:self.FILE_NAME_HASH_LEN]
156
+
157
+ tar_file_key = f'requirements-{requirements_hash}'
158
+ tar_file_name = f'{tar_file_key}.tar'
159
+
160
+ temp_dir = tempfile.mkdtemp()
161
+ self._enter_context(defer(lambda: shutil.rmtree(temp_dir))) # noqa
162
+
163
+ if self._file_cache is not None and (cache_tar_file := self._file_cache.get_file(tar_file_key)):
164
+ with tarfile.open(cache_tar_file) as tar:
165
+ tar.extractall(path=temp_dir) # noqa
166
+
167
+ return temp_dir
168
+
169
+ temp_requirements_dir = os.path.join(temp_dir, 'requirements')
170
+ os.makedirs(temp_requirements_dir)
171
+
172
+ download_requirements(
173
+ self.resolve_ci_image(),
174
+ temp_requirements_dir,
175
+ requirements_txts,
176
+ )
177
+
178
+ if self._file_cache is not None:
179
+ temp_tar_file = os.path.join(temp_dir, tar_file_name)
180
+
181
+ with tarfile.open(temp_tar_file, 'w') as tar:
182
+ for requirement_file in os.listdir(temp_requirements_dir):
183
+ tar.add(
184
+ os.path.join(temp_requirements_dir, requirement_file),
185
+ arcname=requirement_file,
186
+ )
187
+
188
+ self._file_cache.put_file(os.path.basename(tar_file_key), temp_tar_file)
189
+
190
+ return temp_requirements_dir
191
+
192
+ @cached_nullary
193
+ def resolve_requirements_dir(self) -> str:
194
+ with log_timing_context('Resolve requirements dir') as ltc:
195
+ requirements_dir = self._resolve_requirements_dir()
196
+ ltc.set_description(f'Resolve requirements dir: {requirements_dir}')
197
+ return requirements_dir
198
+
199
+ #
200
+
201
+ def _run_compose_(self) -> None:
202
+ setup_cmds = [
203
+ 'pip install --root-user-action ignore --find-links /requirements --no-index uv',
204
+ (
205
+ 'uv pip install --system --find-links /requirements ' +
206
+ ' '.join(f'-r /project/{rf}' for rf in self._cfg.requirements_txts or [])
207
+ ),
208
+ ]
209
+
210
+ #
211
+
212
+ ci_cmd = dc.replace(self._cfg.cmd, s=' && '.join([
213
+ *setup_cmds,
214
+ f'({self._cfg.cmd.s})',
215
+ ]))
216
+
217
+ #
218
+
219
+ with DockerComposeRun(DockerComposeRun.Config(
220
+ compose_file=self._cfg.compose_file,
221
+ service=self._cfg.service,
222
+
223
+ image=self.resolve_ci_image(),
224
+
225
+ cmd=ci_cmd,
226
+
227
+ run_options=[
228
+ '-v', f'{os.path.abspath(self._cfg.project_dir)}:/project',
229
+ '-v', f'{os.path.abspath(self.resolve_requirements_dir())}:/requirements',
230
+ ],
231
+
232
+ cwd=self._cfg.project_dir,
233
+ )) as ci_compose_run:
234
+ ci_compose_run.run()
235
+
236
+ def _run_compose(self) -> None:
237
+ with log_timing_context('Run compose'):
238
+ self._run_compose_()
239
+
240
+ #
241
+
242
+ def run(self) -> None:
243
+ self.load_compose_service_dependencies()
244
+
245
+ self.resolve_ci_image()
246
+
247
+ self.resolve_requirements_dir()
248
+
249
+ self._run_compose()