offline-docker-cli 2.2.2__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.
- offline_docker/__init__.py +3 -0
- offline_docker/__main__.py +4 -0
- offline_docker/actions.py +173 -0
- offline_docker/build.py +245 -0
- offline_docker/cli.py +486 -0
- offline_docker/config.py +692 -0
- offline_docker/configure.py +282 -0
- offline_docker/context.py +111 -0
- offline_docker/defaults.py +48 -0
- offline_docker/discover.py +139 -0
- offline_docker/docker_cmd.py +136 -0
- offline_docker/download_debs.py +142 -0
- offline_docker/download_images.py +26 -0
- offline_docker/download_npm.py +106 -0
- offline_docker/download_trivy.py +90 -0
- offline_docker/download_wheels.py +363 -0
- offline_docker/download_yarn.py +208 -0
- offline_docker/ensure_base.py +61 -0
- offline_docker/hooks.py +84 -0
- offline_docker/init_cmd.py +54 -0
- offline_docker/load_images.py +34 -0
- offline_docker/oc_discovery.py +206 -0
- offline_docker/resolve.py +306 -0
- offline_docker/scan.py +496 -0
- offline_docker/sources/__init__.py +18 -0
- offline_docker/sources/base.py +18 -0
- offline_docker/sources/filesystem.py +17 -0
- offline_docker/sources/nexus.py +61 -0
- offline_docker/sources/nexus_client.py +67 -0
- offline_docker/sources/proget.py +49 -0
- offline_docker/sources/proget_client.py +48 -0
- offline_docker/sources/upstream.py +36 -0
- offline_docker/status.py +247 -0
- offline_docker/sync_cache.py +125 -0
- offline_docker/trivy_scan.py +86 -0
- offline_docker/tui/__init__.py +19 -0
- offline_docker/tui/app.py +56 -0
- offline_docker/tui/runner.py +87 -0
- offline_docker/tui/screens.py +333 -0
- offline_docker/user_config.py +89 -0
- offline_docker/utils.py +73 -0
- offline_docker/validate.py +53 -0
- offline_docker/wheel_prune.py +141 -0
- offline_docker_cli-2.2.2.dist-info/METADATA +417 -0
- offline_docker_cli-2.2.2.dist-info/RECORD +49 -0
- offline_docker_cli-2.2.2.dist-info/WHEEL +5 -0
- offline_docker_cli-2.2.2.dist-info/entry_points.txt +2 -0
- offline_docker_cli-2.2.2.dist-info/licenses/LICENSE +21 -0
- offline_docker_cli-2.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Interactive / non-interactive repository source configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from offline_docker.user_config import (
|
|
10
|
+
local_only_dict,
|
|
11
|
+
online_fill_dict,
|
|
12
|
+
save_user_sources,
|
|
13
|
+
user_sources_path,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Helpful placeholders for operators (shown in prompts; Enter keeps empty).
|
|
17
|
+
_HINT_PROGET_URL = "https://proget.vitebsk.energo.net"
|
|
18
|
+
_HINT_PROGET_PYPI = "ve_pypi"
|
|
19
|
+
_HINT_PROGET_NPM = "npm-ve"
|
|
20
|
+
_HINT_PROGET_DEB = "local-deb"
|
|
21
|
+
_HINT_NEXUS_URL = "http://repos.vitebsk.energo.by:8081"
|
|
22
|
+
_HINT_NEXUS_PYPI = "pypi-group"
|
|
23
|
+
_HINT_NEXUS_NPM = "npm-group"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _prompt(label: str, *, hint: str | None = None) -> str:
|
|
27
|
+
suffix = f" [{hint}]" if hint else ""
|
|
28
|
+
try:
|
|
29
|
+
value = input(f"{label}{suffix}: ").strip()
|
|
30
|
+
except EOFError:
|
|
31
|
+
return ""
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_sources_dict(
|
|
36
|
+
*,
|
|
37
|
+
proget_url: str,
|
|
38
|
+
proget_pypi: str,
|
|
39
|
+
proget_npm: str,
|
|
40
|
+
proget_deb: str,
|
|
41
|
+
nexus_url: str,
|
|
42
|
+
nexus_pypi: str,
|
|
43
|
+
nexus_npm: str,
|
|
44
|
+
nexus_raw: str,
|
|
45
|
+
order: list[str] | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
sources: list[dict[str, Any]] = [
|
|
48
|
+
{"name": "local", "type": "filesystem", "enabled": True},
|
|
49
|
+
]
|
|
50
|
+
resolve_order = ["local"]
|
|
51
|
+
|
|
52
|
+
if proget_url:
|
|
53
|
+
feeds: dict[str, str] = {}
|
|
54
|
+
if proget_pypi:
|
|
55
|
+
feeds["pypi"] = proget_pypi
|
|
56
|
+
if proget_npm:
|
|
57
|
+
feeds["npm"] = proget_npm
|
|
58
|
+
if proget_deb:
|
|
59
|
+
feeds["deb"] = proget_deb
|
|
60
|
+
sources.append(
|
|
61
|
+
{
|
|
62
|
+
"name": "proget",
|
|
63
|
+
"type": "proget",
|
|
64
|
+
"url": proget_url.rstrip("/"),
|
|
65
|
+
"feeds": feeds,
|
|
66
|
+
"auth_env": "PROGET_API_KEY",
|
|
67
|
+
"enabled": True,
|
|
68
|
+
"publish": False,
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
resolve_order.append("proget")
|
|
72
|
+
else:
|
|
73
|
+
sources.append(
|
|
74
|
+
{
|
|
75
|
+
"name": "proget",
|
|
76
|
+
"type": "proget",
|
|
77
|
+
"url": "",
|
|
78
|
+
"feeds": {},
|
|
79
|
+
"auth_env": "PROGET_API_KEY",
|
|
80
|
+
"enabled": False,
|
|
81
|
+
"publish": False,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if nexus_url:
|
|
86
|
+
repos: dict[str, str] = {}
|
|
87
|
+
if nexus_pypi:
|
|
88
|
+
repos["pypi"] = nexus_pypi
|
|
89
|
+
if nexus_npm:
|
|
90
|
+
repos["npm"] = nexus_npm
|
|
91
|
+
if nexus_raw:
|
|
92
|
+
repos["raw"] = nexus_raw
|
|
93
|
+
sources.append(
|
|
94
|
+
{
|
|
95
|
+
"name": "nexus",
|
|
96
|
+
"type": "nexus",
|
|
97
|
+
"url": nexus_url.rstrip("/"),
|
|
98
|
+
"repos": repos,
|
|
99
|
+
"auth_env": "NEXUS_AUTH",
|
|
100
|
+
"enabled": True,
|
|
101
|
+
"publish": False,
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
resolve_order.append("nexus")
|
|
105
|
+
else:
|
|
106
|
+
sources.append(
|
|
107
|
+
{
|
|
108
|
+
"name": "nexus",
|
|
109
|
+
"type": "nexus",
|
|
110
|
+
"url": "",
|
|
111
|
+
"repos": {},
|
|
112
|
+
"auth_env": "NEXUS_AUTH",
|
|
113
|
+
"enabled": False,
|
|
114
|
+
"publish": False,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if order:
|
|
119
|
+
resolve_order = order
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
"sources": sources,
|
|
123
|
+
"resolve": {
|
|
124
|
+
"order": resolve_order,
|
|
125
|
+
"fill_local": True,
|
|
126
|
+
"on_miss": "fail",
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def run_configure(args: argparse.Namespace) -> None:
|
|
132
|
+
if getattr(args, "local_only", False) and getattr(args, "with_upstream", False):
|
|
133
|
+
print("ERROR: укажите либо --local-only, либо --with-upstream", file=sys.stderr)
|
|
134
|
+
raise SystemExit(2)
|
|
135
|
+
|
|
136
|
+
if getattr(args, "local_only", False):
|
|
137
|
+
path = save_user_sources(local_only_dict())
|
|
138
|
+
print(f"Сохранено (local-only): {path}")
|
|
139
|
+
print("Режим для офлайн/корп VM: cache без явного upstream")
|
|
140
|
+
print(" (CLI всё ещё может временно добавить PyPI как fallback — см. WARN).")
|
|
141
|
+
print("Для заполнения кэша с интернета: offdock configure --with-upstream")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
if getattr(args, "with_upstream", False):
|
|
145
|
+
path = save_user_sources(online_fill_dict())
|
|
146
|
+
print(f"Сохранено (local + upstream): {path}")
|
|
147
|
+
print("Resolve order: local, upstream")
|
|
148
|
+
print("PyPI: https://pypi.org/simple | npm: https://registry.npmjs.org/")
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
interactive = sys.stdin.isatty() and not getattr(args, "non_interactive", False)
|
|
152
|
+
|
|
153
|
+
proget_url = (getattr(args, "proget_url", None) or "").strip()
|
|
154
|
+
nexus_url = (getattr(args, "nexus_url", None) or "").strip()
|
|
155
|
+
proget_pypi = (getattr(args, "proget_pypi", None) or "").strip()
|
|
156
|
+
proget_npm = (getattr(args, "proget_npm", None) or "").strip()
|
|
157
|
+
proget_deb = (getattr(args, "proget_deb", None) or "").strip()
|
|
158
|
+
nexus_pypi = (getattr(args, "nexus_pypi", None) or "").strip()
|
|
159
|
+
nexus_npm = (getattr(args, "nexus_npm", None) or "").strip()
|
|
160
|
+
nexus_raw = (getattr(args, "nexus_raw", None) or "").strip()
|
|
161
|
+
order_raw = (getattr(args, "order", None) or "").strip()
|
|
162
|
+
|
|
163
|
+
flags_given = any(
|
|
164
|
+
getattr(args, name) is not None
|
|
165
|
+
for name in (
|
|
166
|
+
"proget_url",
|
|
167
|
+
"nexus_url",
|
|
168
|
+
"proget_pypi",
|
|
169
|
+
"proget_npm",
|
|
170
|
+
"proget_deb",
|
|
171
|
+
"nexus_pypi",
|
|
172
|
+
"nexus_npm",
|
|
173
|
+
"nexus_raw",
|
|
174
|
+
"order",
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if interactive and not flags_given:
|
|
179
|
+
print("Настройка источников пакетов (Enter = пропустить / пусто).")
|
|
180
|
+
print(f"User config будет записан в: {user_sources_path()}")
|
|
181
|
+
print()
|
|
182
|
+
print("— ProGet —")
|
|
183
|
+
proget_url = _prompt("ProGet base URL", hint=_HINT_PROGET_URL)
|
|
184
|
+
if proget_url:
|
|
185
|
+
proget_pypi = _prompt("ProGet PyPI feed", hint=_HINT_PROGET_PYPI) or _HINT_PROGET_PYPI
|
|
186
|
+
proget_npm = _prompt("ProGet npm feed", hint=_HINT_PROGET_NPM) or _HINT_PROGET_NPM
|
|
187
|
+
proget_deb = _prompt("ProGet deb feed", hint=_HINT_PROGET_DEB) or _HINT_PROGET_DEB
|
|
188
|
+
print()
|
|
189
|
+
print("— Nexus —")
|
|
190
|
+
nexus_url = _prompt("Nexus base URL", hint=_HINT_NEXUS_URL)
|
|
191
|
+
if nexus_url:
|
|
192
|
+
nexus_pypi = _prompt("Nexus PyPI repo", hint=_HINT_NEXUS_PYPI) or _HINT_NEXUS_PYPI
|
|
193
|
+
nexus_npm = _prompt("Nexus npm repo", hint=_HINT_NEXUS_NPM) or _HINT_NEXUS_NPM
|
|
194
|
+
nexus_raw = _prompt("Nexus raw repo (optional)", hint="offline-cache-raw")
|
|
195
|
+
print()
|
|
196
|
+
order_raw = _prompt(
|
|
197
|
+
"Resolve order (comma-separated)",
|
|
198
|
+
hint="local,proget,nexus",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Non-interactive defaults for feed names when URL given without feed flags
|
|
202
|
+
if proget_url:
|
|
203
|
+
proget_pypi = proget_pypi or _HINT_PROGET_PYPI
|
|
204
|
+
proget_npm = proget_npm or _HINT_PROGET_NPM
|
|
205
|
+
proget_deb = proget_deb or _HINT_PROGET_DEB
|
|
206
|
+
if nexus_url:
|
|
207
|
+
nexus_pypi = nexus_pypi or _HINT_NEXUS_PYPI
|
|
208
|
+
nexus_npm = nexus_npm or _HINT_NEXUS_NPM
|
|
209
|
+
|
|
210
|
+
order: list[str] | None = None
|
|
211
|
+
if order_raw:
|
|
212
|
+
order = [p.strip() for p in order_raw.split(",") if p.strip()]
|
|
213
|
+
|
|
214
|
+
if not proget_url and not nexus_url:
|
|
215
|
+
# Без корп. реестров на машине с сетью удобнее local+upstream, чем «тихий» local-only.
|
|
216
|
+
data = online_fill_dict()
|
|
217
|
+
path = save_user_sources(data)
|
|
218
|
+
print(f"URL не заданы — сохранён local + upstream (PyPI/npmjs): {path}")
|
|
219
|
+
print("Только offline-cache: offdock configure --local-only")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
data = _build_sources_dict(
|
|
223
|
+
proget_url=proget_url,
|
|
224
|
+
proget_pypi=proget_pypi,
|
|
225
|
+
proget_npm=proget_npm,
|
|
226
|
+
proget_deb=proget_deb,
|
|
227
|
+
nexus_url=nexus_url,
|
|
228
|
+
nexus_pypi=nexus_pypi,
|
|
229
|
+
nexus_npm=nexus_npm,
|
|
230
|
+
nexus_raw=nexus_raw,
|
|
231
|
+
order=order,
|
|
232
|
+
)
|
|
233
|
+
path = save_user_sources(data)
|
|
234
|
+
print(f"Сохранено: {path}")
|
|
235
|
+
print(f"Resolve order: {', '.join(data['resolve']['order'])}")
|
|
236
|
+
print("Auth (опционально): PROGET_API_KEY, NEXUS_AUTH")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def add_configure_parser(sub: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
240
|
+
p = sub.add_parser(
|
|
241
|
+
"configure",
|
|
242
|
+
help="Настроить ProGet/Nexus (user-level sources.json)",
|
|
243
|
+
description=(
|
|
244
|
+
"Интерактивная или флаги-настройка источников пакетов. "
|
|
245
|
+
"--with-upstream = local + PyPI/npmjs; --local-only = только offline-cache; "
|
|
246
|
+
"пустые ProGet/Nexus URL в интерактиве = --with-upstream. "
|
|
247
|
+
"Не требует --project."
|
|
248
|
+
),
|
|
249
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
250
|
+
epilog=(
|
|
251
|
+
"примеры:\n"
|
|
252
|
+
" offdock configure\n"
|
|
253
|
+
" offdock configure --with-upstream # fill cache с PyPI/npmjs\n"
|
|
254
|
+
" offdock configure --local-only # только offline-cache\n"
|
|
255
|
+
" offdock configure --proget-url https://proget.example.com \\\n"
|
|
256
|
+
" --nexus-url http://nexus.example.com:8081\n"
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
p.add_argument(
|
|
260
|
+
"--local-only",
|
|
261
|
+
action="store_true",
|
|
262
|
+
help="Только offline-cache (офлайн/корп VM)",
|
|
263
|
+
)
|
|
264
|
+
p.add_argument(
|
|
265
|
+
"--with-upstream",
|
|
266
|
+
action="store_true",
|
|
267
|
+
help="local + PyPI/npmjs upstream (машина с сетью для cache)",
|
|
268
|
+
)
|
|
269
|
+
p.add_argument("--non-interactive", action="store_true", help="Без prompt (только флаги)")
|
|
270
|
+
p.add_argument("--proget-url", default=None, help="ProGet base URL")
|
|
271
|
+
p.add_argument("--proget-pypi", default=None, help=f"ProGet PyPI feed (hint: {_HINT_PROGET_PYPI})")
|
|
272
|
+
p.add_argument("--proget-npm", default=None, help=f"ProGet npm feed (hint: {_HINT_PROGET_NPM})")
|
|
273
|
+
p.add_argument("--proget-deb", default=None, help=f"ProGet deb feed (hint: {_HINT_PROGET_DEB})")
|
|
274
|
+
p.add_argument("--nexus-url", default=None, help="Nexus base URL")
|
|
275
|
+
p.add_argument("--nexus-pypi", default=None, help=f"Nexus PyPI repo (hint: {_HINT_NEXUS_PYPI})")
|
|
276
|
+
p.add_argument("--nexus-npm", default=None, help=f"Nexus npm repo (hint: {_HINT_NEXUS_NPM})")
|
|
277
|
+
p.add_argument("--nexus-raw", default=None, help="Nexus raw repo for offline-cache mirror")
|
|
278
|
+
p.add_argument(
|
|
279
|
+
"--order",
|
|
280
|
+
default=None,
|
|
281
|
+
help="Resolve order, e.g. local,proget,nexus",
|
|
282
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Runtime paths derived from ProjectConfig."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from offline_docker.config import ProjectConfig, load_config
|
|
9
|
+
from offline_docker.utils import combined_files_hash
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ProjectContext:
|
|
14
|
+
config: ProjectConfig
|
|
15
|
+
wheels_dir: Path
|
|
16
|
+
wheels_hash_file: Path
|
|
17
|
+
debs_dir: Path
|
|
18
|
+
apt_meta_dir: Path
|
|
19
|
+
npm_cache_dir: Path
|
|
20
|
+
docker_cache_dir: Path
|
|
21
|
+
trivy_cache_dir: Path
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def root(self) -> Path:
|
|
25
|
+
return self.config.root
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def offline_cache(self) -> Path:
|
|
29
|
+
return self.config.offline_cache
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def log_dir(self) -> Path:
|
|
33
|
+
return self.config.log_dir
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_config(cls, config: ProjectConfig) -> ProjectContext:
|
|
37
|
+
offline = config.offline_cache
|
|
38
|
+
wheels = offline / "wheels"
|
|
39
|
+
return cls(
|
|
40
|
+
config=config,
|
|
41
|
+
wheels_dir=wheels,
|
|
42
|
+
wheels_hash_file=wheels / ".deps.sha256",
|
|
43
|
+
debs_dir=offline / "debs",
|
|
44
|
+
apt_meta_dir=offline / "apt-meta",
|
|
45
|
+
npm_cache_dir=offline / "npm",
|
|
46
|
+
docker_cache_dir=offline / "docker",
|
|
47
|
+
trivy_cache_dir=offline / config.trivy.cache_subdir,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def load(
|
|
52
|
+
cls,
|
|
53
|
+
root: Path,
|
|
54
|
+
*,
|
|
55
|
+
config_file: Path | None = None,
|
|
56
|
+
) -> ProjectContext:
|
|
57
|
+
config = load_config(root, config_file=config_file)
|
|
58
|
+
return cls.from_config(config)
|
|
59
|
+
|
|
60
|
+
def ensure_offline_dirs(self) -> None:
|
|
61
|
+
for d in (
|
|
62
|
+
self.wheels_dir,
|
|
63
|
+
self.debs_dir,
|
|
64
|
+
self.apt_meta_dir,
|
|
65
|
+
self.npm_cache_dir,
|
|
66
|
+
self.docker_cache_dir,
|
|
67
|
+
self.trivy_cache_dir,
|
|
68
|
+
):
|
|
69
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
from offline_docker.oc_discovery import discover_yarn_cache_specs
|
|
71
|
+
|
|
72
|
+
for spec in discover_yarn_cache_specs(self.config):
|
|
73
|
+
(self.offline_cache / spec.cache_subdir).mkdir(parents=True, exist_ok=True)
|
|
74
|
+
(self.offline_cache / spec.archive_subdir).mkdir(parents=True, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
def ensure_log_dir(self) -> None:
|
|
77
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
def offline_build_log(self, stem: str) -> Path:
|
|
80
|
+
self.ensure_log_dir()
|
|
81
|
+
return self.log_dir / f"offline-build-{stem}.log"
|
|
82
|
+
|
|
83
|
+
def trivy_scan_log(self, target: str) -> Path:
|
|
84
|
+
self.ensure_log_dir()
|
|
85
|
+
return self.log_dir / f"trivy-scan-{target}.log"
|
|
86
|
+
|
|
87
|
+
def python_deps_hash(self) -> str:
|
|
88
|
+
paths = self.config.python_lock_paths()
|
|
89
|
+
if not paths:
|
|
90
|
+
raise RuntimeError("Python lock files not found")
|
|
91
|
+
return combined_files_hash(paths)
|
|
92
|
+
|
|
93
|
+
def set_offline_cache_env(self) -> None:
|
|
94
|
+
import os
|
|
95
|
+
|
|
96
|
+
os.environ.setdefault("DOCKER_BUILDKIT", "1")
|
|
97
|
+
os.environ.setdefault("OFFLINE_CACHE", str(self.offline_cache))
|
|
98
|
+
# Compose additional_contexts on Windows: absolute D:\... often yields empty
|
|
99
|
+
# BuildKit context (oc: 2B) and fails on source=vendor. Prefer ./relative.
|
|
100
|
+
try:
|
|
101
|
+
rel = self.offline_cache.resolve().relative_to(self.root.resolve()).as_posix()
|
|
102
|
+
oc_compose = rel if rel.startswith(".") else f"./{rel}"
|
|
103
|
+
except ValueError:
|
|
104
|
+
oc_compose = self.offline_cache.as_posix()
|
|
105
|
+
app_key = f"{self.config.app_name.upper()}_OFFLINE_CACHE"
|
|
106
|
+
existing = os.environ.get(app_key, "")
|
|
107
|
+
looks_win_abs = bool(existing) and (
|
|
108
|
+
(len(existing) >= 2 and existing[1] == ":") or existing.startswith("\\\\")
|
|
109
|
+
)
|
|
110
|
+
if not existing or looks_win_abs:
|
|
111
|
+
os.environ[app_key] = oc_compose
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Default values and shared types for offline-docker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class WheelPattern:
|
|
10
|
+
pattern: str
|
|
11
|
+
label: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_DEB_PACKAGES = [
|
|
15
|
+
"locales",
|
|
16
|
+
"python3.12",
|
|
17
|
+
"python3.12-venv",
|
|
18
|
+
"python3.12-dev",
|
|
19
|
+
"python3-pip",
|
|
20
|
+
"git",
|
|
21
|
+
"curl",
|
|
22
|
+
"wget",
|
|
23
|
+
"ca-certificates",
|
|
24
|
+
"gnupg",
|
|
25
|
+
"libgl1",
|
|
26
|
+
"libglib2.0-0",
|
|
27
|
+
"libsm6",
|
|
28
|
+
"libxext6",
|
|
29
|
+
"libxrender1",
|
|
30
|
+
"ffmpeg",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
DEFAULT_DOCKER_IMAGES = [
|
|
34
|
+
"ubuntu:24.04",
|
|
35
|
+
"node:22-bookworm-slim",
|
|
36
|
+
"nginx:1.28-alpine",
|
|
37
|
+
"redis:7-alpine",
|
|
38
|
+
"aquasec/trivy:latest",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
DEFAULT_CRITICAL_WHEELS = [
|
|
42
|
+
WheelPattern("PyJWT-*.whl", "PyJWT"),
|
|
43
|
+
WheelPattern("psycopg-*.whl", "psycopg"),
|
|
44
|
+
WheelPattern("psycopg_binary-*.whl", "psycopg_binary"),
|
|
45
|
+
WheelPattern("redis-*.whl", "redis"),
|
|
46
|
+
WheelPattern("sqlalchemy-*.whl", "sqlalchemy"),
|
|
47
|
+
WheelPattern("alembic-*.whl", "alembic"),
|
|
48
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Auto-discovery of Docker/Python/frontend dependencies in a project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from offline_docker.config import ProjectConfig
|
|
8
|
+
from offline_docker.scan import (
|
|
9
|
+
ScanInventory,
|
|
10
|
+
compute_init_gaps,
|
|
11
|
+
inventory_to_draft_config,
|
|
12
|
+
recursive_scan,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def discover_project(root: Path) -> ProjectConfig:
|
|
17
|
+
"""Recursive scan + role heuristics (shared with init)."""
|
|
18
|
+
root = root.resolve()
|
|
19
|
+
inventory = recursive_scan(root)
|
|
20
|
+
return inventory_to_draft_config(root, inventory)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def discover_with_inventory(root: Path) -> tuple[ProjectConfig, ScanInventory]:
|
|
24
|
+
root = root.resolve()
|
|
25
|
+
inventory = recursive_scan(root)
|
|
26
|
+
cfg = inventory_to_draft_config(root, inventory)
|
|
27
|
+
return cfg, inventory
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_discovery_report(
|
|
31
|
+
cfg: ProjectConfig,
|
|
32
|
+
*,
|
|
33
|
+
inventory: ScanInventory | None = None,
|
|
34
|
+
gaps: list[str] | None = None,
|
|
35
|
+
) -> str:
|
|
36
|
+
if inventory is None:
|
|
37
|
+
inventory = recursive_scan(cfg.root)
|
|
38
|
+
if gaps is None:
|
|
39
|
+
gaps = compute_init_gaps(cfg, inventory)
|
|
40
|
+
|
|
41
|
+
lines = [
|
|
42
|
+
f"=== discover: {cfg.root} ===",
|
|
43
|
+
"",
|
|
44
|
+
f"Конфиг: {cfg.config_source or '(не задан)'}",
|
|
45
|
+
f"App name: {cfg.app_name}",
|
|
46
|
+
f"Offline cache: {cfg.offline_cache}",
|
|
47
|
+
f"Log dir: {cfg.log_dir}",
|
|
48
|
+
"",
|
|
49
|
+
"Compose:",
|
|
50
|
+
]
|
|
51
|
+
for f in cfg.compose_files or ["(не найдены)"]:
|
|
52
|
+
lines.append(f" - {f}")
|
|
53
|
+
lines.extend(
|
|
54
|
+
[
|
|
55
|
+
f" online: {cfg.compose_online}",
|
|
56
|
+
f" offline: {cfg.compose_offline}",
|
|
57
|
+
"",
|
|
58
|
+
"Python lock files:",
|
|
59
|
+
]
|
|
60
|
+
)
|
|
61
|
+
for f in cfg.python_lock_files:
|
|
62
|
+
exists = "ok" if cfg.path(f).is_file() else "missing"
|
|
63
|
+
lines.append(f" - {f} ({exists})")
|
|
64
|
+
tv = cfg.torch_version()
|
|
65
|
+
lines.extend(
|
|
66
|
+
[
|
|
67
|
+
f" torch: {tv or '(не найден)'}",
|
|
68
|
+
f" uv export: {cfg.use_uv_export}",
|
|
69
|
+
f" project dir: {cfg.effective_python_project_dir()}",
|
|
70
|
+
"",
|
|
71
|
+
"Yarn cache (oc source=yarn):",
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
from offline_docker.oc_discovery import discover_yarn_cache_specs
|
|
75
|
+
|
|
76
|
+
yarn_specs = discover_yarn_cache_specs(cfg)
|
|
77
|
+
for spec in yarn_specs:
|
|
78
|
+
lines.append(
|
|
79
|
+
f" - cache={spec.cache_subdir}, archive={spec.archive_subdir}, "
|
|
80
|
+
f"arg={spec.version_arg}, dockerfile={spec.source_dockerfile}"
|
|
81
|
+
)
|
|
82
|
+
if not yarn_specs:
|
|
83
|
+
lines.append(" (не требуется)")
|
|
84
|
+
lines.extend(
|
|
85
|
+
[
|
|
86
|
+
"",
|
|
87
|
+
f"Frontend: {cfg.frontend_dir}/",
|
|
88
|
+
f" lock: {cfg.frontend_lock}",
|
|
89
|
+
"",
|
|
90
|
+
"Dockerfiles:",
|
|
91
|
+
f" base: {cfg.dockerfiles.base}",
|
|
92
|
+
f" base_offline: {cfg.dockerfiles.base_offline}",
|
|
93
|
+
f" backend: {cfg.dockerfiles.backend}",
|
|
94
|
+
f" backend_offline: {cfg.dockerfiles.backend_offline}",
|
|
95
|
+
f" frontend: {cfg.dockerfiles.frontend}",
|
|
96
|
+
f" frontend_offline: {cfg.dockerfiles.frontend_offline}",
|
|
97
|
+
"",
|
|
98
|
+
f"APT packages ({len(cfg.deb_packages)}):",
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
for pkg in cfg.deb_packages[:15]:
|
|
102
|
+
lines.append(f" - {pkg}")
|
|
103
|
+
if len(cfg.deb_packages) > 15:
|
|
104
|
+
lines.append(f" ... и ещё {len(cfg.deb_packages) - 15}")
|
|
105
|
+
lines.extend(["", "Docker images:"])
|
|
106
|
+
for img in cfg.docker_images:
|
|
107
|
+
lines.append(f" - {img}")
|
|
108
|
+
lines.extend(
|
|
109
|
+
[
|
|
110
|
+
"",
|
|
111
|
+
"Image tags:",
|
|
112
|
+
f" base: {cfg.images.base}",
|
|
113
|
+
f" backend: {cfg.images.backend}",
|
|
114
|
+
f" frontend: {cfg.images.frontend}",
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
if cfg.deb_extra_repos:
|
|
118
|
+
lines.extend(["", "Extra APT repos:"])
|
|
119
|
+
for repo in cfg.deb_extra_repos:
|
|
120
|
+
lines.append(f" - {repo.name}")
|
|
121
|
+
|
|
122
|
+
if inventory.dockerfiles or inventory.python_files or inventory.package_json_dirs:
|
|
123
|
+
lines.extend(["", "Scan inventory (recursive):"])
|
|
124
|
+
lines.append(f" compose: {len(inventory.compose_files)}")
|
|
125
|
+
lines.append(f" dockerfiles:{len(inventory.dockerfiles)}")
|
|
126
|
+
lines.append(f" python: {len(inventory.python_files)}")
|
|
127
|
+
lines.append(f" frontend locks: {len(inventory.frontend_lock_paths)}")
|
|
128
|
+
|
|
129
|
+
lines.extend(["", "Missing / to fix:"])
|
|
130
|
+
if gaps:
|
|
131
|
+
for g in gaps:
|
|
132
|
+
lines.append(f" - {g}")
|
|
133
|
+
else:
|
|
134
|
+
lines.append(" (нет — offline-артефакты на месте)")
|
|
135
|
+
|
|
136
|
+
lines.extend(["", "Package sources:"])
|
|
137
|
+
lines.extend(cfg.package_resolver().summary_lines())
|
|
138
|
+
|
|
139
|
+
return "\n".join(lines)
|