adam-community 1.0.24__tar.gz → 1.0.26__tar.gz
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.
- {adam_community-1.0.24 → adam_community-1.0.26}/PKG-INFO +19 -1
- {adam_community-1.0.24 → adam_community-1.0.26}/README.md +18 -0
- adam_community-1.0.26/adam_community/__version__.py +1 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/cli.py +4 -0
- adam_community-1.0.26/adam_community/cli/sif_build.py +576 -0
- adam_community-1.0.26/adam_community/cli/templates/agent_python.py.j2 +334 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community.egg-info/PKG-INFO +19 -1
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community.egg-info/SOURCES.txt +2 -0
- adam_community-1.0.26/test/test_sif_upload.py +444 -0
- adam_community-1.0.24/adam_community/__version__.py +0 -1
- adam_community-1.0.24/adam_community/cli/templates/agent_python.py.j2 +0 -139
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/__init__.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/__init__.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/build.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/init.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/parser.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/Makefile.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/README_agent.md.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/README_kit.md.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/__init__.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/configure.json.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/initial_assistant_message.md.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/initial_assistant_message_en.md.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/initial_system_prompt.md.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/initial_system_prompt_en.md.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/input.json.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/kit_python.py.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/long_description.md.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/long_description_en.md.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/templates/rag_python.py.j2 +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/cli/updater.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/tool.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community/util.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community.egg-info/dependency_links.txt +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community.egg-info/entry_points.txt +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community.egg-info/requires.txt +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/adam_community.egg-info/top_level.txt +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/setup.cfg +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/setup.py +0 -0
- {adam_community-1.0.24 → adam_community-1.0.26}/test/test_util_tool.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: adam_community
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.26
|
|
4
4
|
Summary: Adam Community Tools and Utilities
|
|
5
5
|
Home-page: https://github.com/yourusername/adam-community
|
|
6
6
|
Author: Adam Community
|
|
@@ -67,6 +67,23 @@ adam-cli build .
|
|
|
67
67
|
adam-cli update
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
#### SIF 文件管理
|
|
71
|
+
|
|
72
|
+
管理 SIF 文件,包括上传到 Docker 镜像仓库等操作:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# 查看帮助
|
|
76
|
+
adam-cli sif --help
|
|
77
|
+
|
|
78
|
+
# 上传 SIF 文件到镜像仓库(自适应切片)
|
|
79
|
+
adam-cli sif upload ./xxx.sif registry.example.com/image:1.0.0
|
|
80
|
+
|
|
81
|
+
# 带认证上传
|
|
82
|
+
adam-cli sif upload ./app.sif registry.cn-hangzhou.aliyuncs.com/ns/app:latest \
|
|
83
|
+
--username user --password pass
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
|
|
70
87
|
### Python 模块导入
|
|
71
88
|
|
|
72
89
|
```python
|
|
@@ -116,6 +133,7 @@ trackPath("/path/to/cache")
|
|
|
116
133
|
- **项目构建**: 检查配置文件、文档文件并创建 zip 包
|
|
117
134
|
- **类型检查**: 支持多种 Python 类型注解格式
|
|
118
135
|
- **自动更新**: 智能检查和更新到最新版本,支持用户配置
|
|
136
|
+
- **SIF 镜像构建**: 将 SIF 文件切片并构建 Docker 镜像推送到仓库
|
|
119
137
|
|
|
120
138
|
## 开发
|
|
121
139
|
|
|
@@ -37,6 +37,23 @@ adam-cli build .
|
|
|
37
37
|
adam-cli update
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
#### SIF 文件管理
|
|
41
|
+
|
|
42
|
+
管理 SIF 文件,包括上传到 Docker 镜像仓库等操作:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 查看帮助
|
|
46
|
+
adam-cli sif --help
|
|
47
|
+
|
|
48
|
+
# 上传 SIF 文件到镜像仓库(自适应切片)
|
|
49
|
+
adam-cli sif upload ./xxx.sif registry.example.com/image:1.0.0
|
|
50
|
+
|
|
51
|
+
# 带认证上传
|
|
52
|
+
adam-cli sif upload ./app.sif registry.cn-hangzhou.aliyuncs.com/ns/app:latest \
|
|
53
|
+
--username user --password pass
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
|
|
40
57
|
### Python 模块导入
|
|
41
58
|
|
|
42
59
|
```python
|
|
@@ -86,6 +103,7 @@ trackPath("/path/to/cache")
|
|
|
86
103
|
- **项目构建**: 检查配置文件、文档文件并创建 zip 包
|
|
87
104
|
- **类型检查**: 支持多种 Python 类型注解格式
|
|
88
105
|
- **自动更新**: 智能检查和更新到最新版本,支持用户配置
|
|
106
|
+
- **SIF 镜像构建**: 将 SIF 文件切片并构建 Docker 镜像推送到仓库
|
|
89
107
|
|
|
90
108
|
## 开发
|
|
91
109
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.26"
|
|
@@ -5,6 +5,7 @@ from .parser import parse_directory
|
|
|
5
5
|
from .build import build_package
|
|
6
6
|
from .init import init
|
|
7
7
|
from .updater import check_and_notify_update, update_cli, set_update_disabled
|
|
8
|
+
from .sif_build import sif
|
|
8
9
|
from ..__version__ import __version__
|
|
9
10
|
|
|
10
11
|
@click.group()
|
|
@@ -76,5 +77,8 @@ def config(disable_update_check, enable_update_check):
|
|
|
76
77
|
# 添加 init 命令
|
|
77
78
|
cli.add_command(init)
|
|
78
79
|
|
|
80
|
+
# 添加 sif 命令组
|
|
81
|
+
cli.add_command(sif)
|
|
82
|
+
|
|
79
83
|
if __name__ == '__main__':
|
|
80
84
|
cli()
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Tuple, List, Optional
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validateSifFile(sif_path: Path) -> Tuple[bool, str]:
|
|
16
|
+
"""验证 SIF 文件是否有效
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
sif_path: SIF 文件路径
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Tuple[bool, str]: (是否有效, 错误消息)
|
|
23
|
+
"""
|
|
24
|
+
if not sif_path.exists():
|
|
25
|
+
return False, f"SIF 文件不存在: {sif_path}"
|
|
26
|
+
|
|
27
|
+
if not sif_path.is_file():
|
|
28
|
+
return False, f"路径不是文件: {sif_path}"
|
|
29
|
+
|
|
30
|
+
if not os.access(sif_path, os.R_OK):
|
|
31
|
+
return False, f"SIF 文件不可读: {sif_path}"
|
|
32
|
+
|
|
33
|
+
file_size = sif_path.stat().st_size
|
|
34
|
+
if file_size == 0:
|
|
35
|
+
return False, f"SIF 文件为空: {sif_path}"
|
|
36
|
+
|
|
37
|
+
return True, ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validateImageUrl(image_url: str) -> Tuple[bool, str]:
|
|
41
|
+
"""验证 Docker 镜像 URL 格式
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
image_url: Docker 镜像 URL
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Tuple[bool, str]: (是否有效, 错误消息)
|
|
48
|
+
"""
|
|
49
|
+
# 基本格式验证:registry/namespace/image:tag
|
|
50
|
+
# 支持域名和 IP 地址(带端口号)
|
|
51
|
+
pattern = r'^[a-zA-Z0-9\-\.]+(:[0-9]+)?(/[a-zA-Z0-9\-_]+)+:[a-zA-Z0-9\.\-_]+$'
|
|
52
|
+
|
|
53
|
+
if not re.match(pattern, image_url):
|
|
54
|
+
return False, "镜像 URL 格式不正确,应为 registry/namespace/image:tag"
|
|
55
|
+
|
|
56
|
+
return True, ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def checkCommandAvailable(command: str) -> Tuple[bool, str, str]:
|
|
60
|
+
"""检查命令是否可用
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
command: 命令名称
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple[bool, str, str]: (是否可用, 安装提示, URL)
|
|
67
|
+
"""
|
|
68
|
+
# 使用 which 命令检测命令是否存在(更可靠)
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
['which', command],
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
timeout=5
|
|
75
|
+
)
|
|
76
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
77
|
+
return True, "", ""
|
|
78
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# 如果 which 不可用,尝试直接运行命令
|
|
82
|
+
try:
|
|
83
|
+
# 对于 split,尝试运行一个简单的命令
|
|
84
|
+
if command == 'split':
|
|
85
|
+
# 使用 --help 而不是 --version(macOS 的 split 不支持 --version)
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
['split', '--help'],
|
|
88
|
+
capture_output=True,
|
|
89
|
+
text=True,
|
|
90
|
+
timeout=5
|
|
91
|
+
)
|
|
92
|
+
if result.returncode == 0:
|
|
93
|
+
return True, "", ""
|
|
94
|
+
else:
|
|
95
|
+
# 其他命令尝试 --version
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
[command, '--version'],
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
timeout=5
|
|
101
|
+
)
|
|
102
|
+
if result.returncode == 0:
|
|
103
|
+
return True, "", ""
|
|
104
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
# 命令不可用,返回安装提示
|
|
108
|
+
install_hints = {
|
|
109
|
+
'split': (
|
|
110
|
+
"split 命令未找到",
|
|
111
|
+
"split 是 macOS/Linux 系统自带命令\n\n"
|
|
112
|
+
"macOS: 已预装(如果提示缺少,请安装 Xcode Command Line Tools)\n"
|
|
113
|
+
" xcode-select --install\n\n"
|
|
114
|
+
"Linux: sudo apt-get install coreutils / sudo yum install coreutils"
|
|
115
|
+
),
|
|
116
|
+
'docker': (
|
|
117
|
+
"Docker 未安装或未运行",
|
|
118
|
+
"Docker 是容器化平台\n\n"
|
|
119
|
+
"macOS: 下载并安装 Docker Desktop\n"
|
|
120
|
+
" https://www.docker.com/products/docker-desktop/\n\n"
|
|
121
|
+
"Linux: 安装 Docker Engine\n"
|
|
122
|
+
" https://docs.docker.com/engine/install/\n\n"
|
|
123
|
+
"安装后请确保 Docker daemon 正在运行"
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if command in install_hints:
|
|
128
|
+
hint, url = install_hints[command]
|
|
129
|
+
return False, hint, url
|
|
130
|
+
|
|
131
|
+
return False, f"{command} 命令未找到", ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def checkDockerEnvironment() -> Tuple[bool, List[str]]:
|
|
135
|
+
"""检查 Docker 是否可用
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Tuple[bool, List[str]]: (是否可用, 错误消息列表)
|
|
139
|
+
"""
|
|
140
|
+
errors = []
|
|
141
|
+
|
|
142
|
+
# 检查 Docker 命令
|
|
143
|
+
docker_available, docker_hint, docker_url = checkCommandAvailable('docker')
|
|
144
|
+
if not docker_available:
|
|
145
|
+
errors.append(f"❌ {docker_hint}")
|
|
146
|
+
if docker_url:
|
|
147
|
+
errors.append(f"\n{docker_url}")
|
|
148
|
+
else:
|
|
149
|
+
# 检查 Docker daemon 是否运行
|
|
150
|
+
try:
|
|
151
|
+
result = subprocess.run(
|
|
152
|
+
['docker', 'info'],
|
|
153
|
+
capture_output=True,
|
|
154
|
+
text=True,
|
|
155
|
+
timeout=5
|
|
156
|
+
)
|
|
157
|
+
if result.returncode != 0:
|
|
158
|
+
errors.append("\n⚠️ Docker daemon 未运行,请启动 Docker")
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
return len(errors) == 0, errors
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def checkRequiredCommands() -> Tuple[bool, List[str]]:
|
|
166
|
+
"""检查所有必需的命令
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Tuple[bool, List[str]]: (是否全部可用, 错误消息列表)
|
|
170
|
+
"""
|
|
171
|
+
all_errors = []
|
|
172
|
+
|
|
173
|
+
# 检查 split 命令
|
|
174
|
+
split_available, split_hint, split_url = checkCommandAvailable('split')
|
|
175
|
+
if not split_available:
|
|
176
|
+
error_msg = f"❌ {split_hint}"
|
|
177
|
+
if split_url:
|
|
178
|
+
error_msg += f"\n\n{split_url}"
|
|
179
|
+
all_errors.append(error_msg)
|
|
180
|
+
|
|
181
|
+
# 检查 Docker
|
|
182
|
+
docker_available, docker_errors = checkDockerEnvironment()
|
|
183
|
+
if not docker_available:
|
|
184
|
+
all_errors.extend(docker_errors)
|
|
185
|
+
|
|
186
|
+
return len(all_errors) == 0, all_errors
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def createWorkDir(sif_path: Path) -> Path:
|
|
190
|
+
"""创建临时工作目录
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
sif_path: SIF 文件路径
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Path: 工作目录路径
|
|
197
|
+
"""
|
|
198
|
+
parent_dir = sif_path.parent
|
|
199
|
+
work_dir = parent_dir / ".sif_build_temp"
|
|
200
|
+
work_dir.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
return work_dir
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def calculateOptimalChunkSize(file_size_bytes: int) -> Optional[str]:
|
|
205
|
+
"""根据文件大小自适应计算切片大小
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
file_size_bytes: 文件大小(字节)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Optional[str]: 切片大小(如 '100M', '500M'),None 表示不切片
|
|
212
|
+
"""
|
|
213
|
+
size_mb = file_size_bytes / (1024 * 1024)
|
|
214
|
+
|
|
215
|
+
if size_mb < 500:
|
|
216
|
+
return None # 不切片
|
|
217
|
+
elif size_mb < 2 * 1024:
|
|
218
|
+
return "100M"
|
|
219
|
+
elif size_mb < 10 * 1024:
|
|
220
|
+
return "500M"
|
|
221
|
+
else:
|
|
222
|
+
return "1G"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def splitSifFile(sif_path: Path, chunk_size: Optional[str], work_dir: Path) -> List[Path]:
|
|
226
|
+
"""切片 SIF 文件
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
sif_path: SIF 文件路径
|
|
230
|
+
chunk_size: 切片大小(如 '100M'),None 表示不切片
|
|
231
|
+
work_dir: 工作目录
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List[Path]: 切片文件列表
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
subprocess.CalledProcessError: split 命令执行失败
|
|
238
|
+
"""
|
|
239
|
+
if chunk_size is None:
|
|
240
|
+
# 不切片,直接复制到工作目录
|
|
241
|
+
dest_file = work_dir / sif_path.name
|
|
242
|
+
shutil.copy2(sif_path, dest_file)
|
|
243
|
+
return [dest_file]
|
|
244
|
+
|
|
245
|
+
# 使用 split 命令切片
|
|
246
|
+
output_prefix = sif_path.name # 不包含路径
|
|
247
|
+
cmd = ['split', '-b', chunk_size, '-d', str(sif_path), output_prefix + '.']
|
|
248
|
+
|
|
249
|
+
result = subprocess.run(
|
|
250
|
+
cmd,
|
|
251
|
+
cwd=work_dir,
|
|
252
|
+
capture_output=True,
|
|
253
|
+
text=True,
|
|
254
|
+
check=True
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# 查找所有切片文件
|
|
258
|
+
chunks = sorted(work_dir.glob(f"{output_prefix}.*"))
|
|
259
|
+
return chunks
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def generateDockerfile(work_dir: Path) -> Path:
|
|
263
|
+
"""生成 Dockerfile
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
work_dir: 工作目录
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Path: Dockerfile 文件路径
|
|
270
|
+
"""
|
|
271
|
+
dockerfile_path = work_dir / "Dockerfile"
|
|
272
|
+
|
|
273
|
+
dockerfile_content = """FROM alpine
|
|
274
|
+
COPY . /sif
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
with open(dockerfile_path, 'w', encoding='utf-8') as f:
|
|
278
|
+
f.write(dockerfile_content)
|
|
279
|
+
|
|
280
|
+
return dockerfile_path
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def executeCommand(cmd: List[str], description: str, console: Console) -> bool:
|
|
284
|
+
"""执行命令并实时显示输出
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
cmd: 命令列表
|
|
288
|
+
description: 命令描述
|
|
289
|
+
console: Console 实例
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
bool: 是否执行成功
|
|
293
|
+
"""
|
|
294
|
+
console.print(f"\n[dim]执行命令: {' '.join(cmd)}[/dim]")
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
process = subprocess.Popen(
|
|
298
|
+
cmd,
|
|
299
|
+
stdout=subprocess.PIPE,
|
|
300
|
+
stderr=subprocess.STDOUT,
|
|
301
|
+
universal_newlines=True,
|
|
302
|
+
bufsize=1
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# 实时显示输出
|
|
306
|
+
for line in process.stdout:
|
|
307
|
+
console.print(line.rstrip())
|
|
308
|
+
|
|
309
|
+
process.wait()
|
|
310
|
+
return process.returncode == 0
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
console.print(f"[red]命令执行异常: {str(e)}[/red]")
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def buildDockerImage(work_dir: Path, image_url: str, console: Console) -> bool:
|
|
318
|
+
"""构建 Docker 镜像
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
work_dir: 工作目录
|
|
322
|
+
image_url: 镜像 URL
|
|
323
|
+
console: Console 实例
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
bool: 是否构建成功
|
|
327
|
+
"""
|
|
328
|
+
# 指定架构为 x86_64,确保在不同平台上的兼容性
|
|
329
|
+
cmd = ['docker', 'build', '--platform', 'linux/amd64', '-t', image_url, str(work_dir)]
|
|
330
|
+
return executeCommand(cmd, "构建 Docker 镜像", console)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def pushDockerImage(image_url: str, username: Optional[str], password: Optional[str], console: Console) -> bool:
|
|
334
|
+
"""推送 Docker 镜像
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
image_url: 镜像 URL
|
|
338
|
+
username: 用户名
|
|
339
|
+
password: 密码
|
|
340
|
+
console: Console 实例
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
bool: 是否推送成功
|
|
344
|
+
"""
|
|
345
|
+
# 如果提供了认证信息,先执行 docker login
|
|
346
|
+
if username and password:
|
|
347
|
+
console.print("\n[bold blue]🔐 登录 Docker 仓库[/bold blue]")
|
|
348
|
+
|
|
349
|
+
# 从镜像 URL 提取 registry
|
|
350
|
+
registry = image_url.split('/')[0]
|
|
351
|
+
|
|
352
|
+
# 使用 stdin 传递密码
|
|
353
|
+
cmd = ['docker', 'login', '-u', username, '--password-stdin', registry]
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
process = subprocess.Popen(
|
|
357
|
+
cmd,
|
|
358
|
+
stdin=subprocess.PIPE,
|
|
359
|
+
stdout=subprocess.PIPE,
|
|
360
|
+
stderr=subprocess.PIPE,
|
|
361
|
+
universal_newlines=True
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
stdout, stderr = process.communicate(input=password)
|
|
365
|
+
|
|
366
|
+
if process.returncode != 0:
|
|
367
|
+
console.print(f"[red]登录失败: {stderr}[/red]")
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
console.print("[green]✓ 登录成功[/green]")
|
|
371
|
+
except Exception as e:
|
|
372
|
+
console.print(f"[red]登录异常: {str(e)}[/red]")
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
# 推送镜像
|
|
376
|
+
cmd = ['docker', 'push', image_url]
|
|
377
|
+
return executeCommand(cmd, "推送 Docker 镜像", console)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def cleanupTempFiles(work_dir: Path, keep_temp: bool, console: Console):
|
|
381
|
+
"""清理临时文件
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
work_dir: 工作目录
|
|
385
|
+
keep_temp: 是否保留临时文件
|
|
386
|
+
console: Console 实例
|
|
387
|
+
"""
|
|
388
|
+
if keep_temp:
|
|
389
|
+
console.print(f"\n[dim]临时文件保留在: {work_dir}[/dim]")
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
console.print(f"\n[bold blue]🧹 清理临时文件[/bold blue]")
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
shutil.rmtree(work_dir)
|
|
396
|
+
console.print(f"[green]✓ 已清理: {work_dir}[/green]")
|
|
397
|
+
except Exception as e:
|
|
398
|
+
console.print(f"[yellow]⚠️ 清理失败: {str(e)}[/yellow]")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@click.group()
|
|
402
|
+
def sif():
|
|
403
|
+
"""SIF 文件管理命令"""
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@sif.command(name='upload')
|
|
408
|
+
@click.argument('sif_file', type=click.Path(exists=True))
|
|
409
|
+
@click.argument('image_url')
|
|
410
|
+
@click.option('--username', help='Docker 仓库用户名')
|
|
411
|
+
@click.option('--password', help='Docker 仓库密码')
|
|
412
|
+
@click.option('--chunk-size', help='覆盖自适应计算的切片大小(如 100M, 500M)')
|
|
413
|
+
@click.option('--keep-temp', is_flag=True, help='保留临时文件')
|
|
414
|
+
def upload(sif_file, image_url, username, password, chunk_size, keep_temp):
|
|
415
|
+
"""将 SIF 文件切片并构建 Docker 镜像推送到仓库
|
|
416
|
+
|
|
417
|
+
切片策略:
|
|
418
|
+
- 文件 < 500MB:不切片,直接使用原文件
|
|
419
|
+
- 文件 500MB-2GB:切片大小 100MB
|
|
420
|
+
- 文件 2GB-10GB:切片大小 500MB
|
|
421
|
+
- 文件 > 10GB:切片大小 1GB
|
|
422
|
+
|
|
423
|
+
可通过 --chunk-size 参数覆盖自适应策略
|
|
424
|
+
|
|
425
|
+
示例:
|
|
426
|
+
adam-cli sif upload ./xxx.sif xxx.cn-hangzhou.cr.aliyuncs.com/openscore/openscore-core:1.0.0
|
|
427
|
+
adam-cli sif upload ./xxx.sif registry.example.com/myimage:latest --username user --password pass
|
|
428
|
+
"""
|
|
429
|
+
sif_path = Path(sif_file).resolve()
|
|
430
|
+
file_size = sif_path.stat().st_size
|
|
431
|
+
file_size_mb = file_size / (1024 * 1024)
|
|
432
|
+
|
|
433
|
+
# 显示开始面板
|
|
434
|
+
console.print(Panel.fit(
|
|
435
|
+
f"[bold blue]🚀 开始构建 SIF Docker 镜像[/bold blue]\n"
|
|
436
|
+
f"SIF 文件: {sif_path.name}\n"
|
|
437
|
+
f"文件大小: {file_size_mb:.2f} MB\n"
|
|
438
|
+
f"目标镜像: {image_url}",
|
|
439
|
+
border_style="blue"
|
|
440
|
+
))
|
|
441
|
+
|
|
442
|
+
work_dir = None
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
# ===== 步骤 1: 验证环境 =====
|
|
446
|
+
console.print("\n[bold blue]📦 步骤 1/5: 验证环境[/bold blue]")
|
|
447
|
+
|
|
448
|
+
# 验证 SIF 文件
|
|
449
|
+
valid, error_msg = validateSifFile(sif_path)
|
|
450
|
+
if not valid:
|
|
451
|
+
console.print(Panel.fit(
|
|
452
|
+
f"[bold red]❌ SIF 文件验证失败[/bold red]\n{error_msg}",
|
|
453
|
+
border_style="red"
|
|
454
|
+
))
|
|
455
|
+
sys.exit(1)
|
|
456
|
+
console.print(" ✓ SIF 文件可读")
|
|
457
|
+
|
|
458
|
+
# 验证镜像 URL
|
|
459
|
+
valid, error_msg = validateImageUrl(image_url)
|
|
460
|
+
if not valid:
|
|
461
|
+
console.print(Panel.fit(
|
|
462
|
+
f"[bold red]❌ 镜像 URL 验证失败[/bold red]\n{error_msg}",
|
|
463
|
+
border_style="red"
|
|
464
|
+
))
|
|
465
|
+
sys.exit(1)
|
|
466
|
+
console.print(" ✓ 镜像 URL 格式正确")
|
|
467
|
+
|
|
468
|
+
# 检查所有必需的命令
|
|
469
|
+
all_available, errors = checkRequiredCommands()
|
|
470
|
+
if not all_available:
|
|
471
|
+
console.print(Panel.fit(
|
|
472
|
+
"[bold red]❌ 环境检查失败[/bold red]\n"
|
|
473
|
+
+ "\n".join(errors),
|
|
474
|
+
border_style="red"
|
|
475
|
+
))
|
|
476
|
+
sys.exit(1)
|
|
477
|
+
console.print(" ✓ split 命令可用")
|
|
478
|
+
console.print(" ✓ Docker 已安装并运行")
|
|
479
|
+
|
|
480
|
+
# ===== 步骤 2: 创建工作目录 =====
|
|
481
|
+
console.print("\n[bold blue]📦 步骤 2/5: 创建工作目录[/bold blue]")
|
|
482
|
+
work_dir = createWorkDir(sif_path)
|
|
483
|
+
console.print(f" ✓ 工作目录: {work_dir}")
|
|
484
|
+
|
|
485
|
+
# ===== 步骤 3: 切片 SIF 文件 =====
|
|
486
|
+
console.print("\n[bold blue]📦 步骤 3/5: 切片 SIF 文件[/bold blue]")
|
|
487
|
+
|
|
488
|
+
# 计算切片大小
|
|
489
|
+
if chunk_size:
|
|
490
|
+
actual_chunk_size = chunk_size
|
|
491
|
+
console.print(f" 切片策略: 自定义 ({chunk_size})")
|
|
492
|
+
else:
|
|
493
|
+
actual_chunk_size = calculateOptimalChunkSize(file_size)
|
|
494
|
+
if actual_chunk_size:
|
|
495
|
+
console.print(f" 切片策略: 自适应 ({actual_chunk_size})")
|
|
496
|
+
else:
|
|
497
|
+
console.print(f" 切片策略: 不切片 (文件 < 500MB)")
|
|
498
|
+
|
|
499
|
+
console.print(f" 文件大小: {file_size_mb:.2f} MB")
|
|
500
|
+
|
|
501
|
+
# 执行切片
|
|
502
|
+
try:
|
|
503
|
+
chunks = splitSifFile(sif_path, actual_chunk_size, work_dir)
|
|
504
|
+
console.print(f" ✓ 切片完成: {len(chunks)} 个文件")
|
|
505
|
+
|
|
506
|
+
# 显示切片详情
|
|
507
|
+
console.print(f"\n 📄 切片详情:")
|
|
508
|
+
for chunk in chunks:
|
|
509
|
+
chunk_size_mb = chunk.stat().st_size / (1024 * 1024)
|
|
510
|
+
console.print(f" - {chunk.name} ({chunk_size_mb:.2f} MB)")
|
|
511
|
+
except subprocess.CalledProcessError as e:
|
|
512
|
+
console.print(Panel.fit(
|
|
513
|
+
f"[bold red]❌ SIF 文件切片失败[/bold red]\n"
|
|
514
|
+
f"错误: {e.stderr}",
|
|
515
|
+
border_style="red"
|
|
516
|
+
))
|
|
517
|
+
cleanupTempFiles(work_dir, keep_temp, console)
|
|
518
|
+
sys.exit(1)
|
|
519
|
+
|
|
520
|
+
# ===== 步骤 4: 生成 Dockerfile =====
|
|
521
|
+
console.print("\n[bold blue]📦 步骤 4/5: 生成 Dockerfile[/bold blue]")
|
|
522
|
+
dockerfile_path = generateDockerfile(work_dir)
|
|
523
|
+
console.print(f" ✓ Dockerfile 已生成")
|
|
524
|
+
|
|
525
|
+
# ===== 步骤 5: 构建 Docker 镜像 =====
|
|
526
|
+
console.print("\n[bold blue]📦 步骤 5/5: 构建 Docker 镜像[/bold blue]")
|
|
527
|
+
|
|
528
|
+
if not buildDockerImage(work_dir, image_url, console):
|
|
529
|
+
console.print(Panel.fit(
|
|
530
|
+
f"[bold red]❌ Docker 镜像构建失败[/bold red]",
|
|
531
|
+
border_style="red"
|
|
532
|
+
))
|
|
533
|
+
cleanupTempFiles(work_dir, keep_temp, console)
|
|
534
|
+
sys.exit(1)
|
|
535
|
+
|
|
536
|
+
console.print("[green] ✓ 镜像构建成功[/green]")
|
|
537
|
+
|
|
538
|
+
# ===== 步骤 6: 推送 Docker 镜像 =====
|
|
539
|
+
console.print("\n[bold blue]📦 步骤 6/6: 推送 Docker 镜像[/bold blue]")
|
|
540
|
+
|
|
541
|
+
if not pushDockerImage(image_url, username, password, console):
|
|
542
|
+
console.print(Panel.fit(
|
|
543
|
+
f"[bold red]❌ Docker 镜像推送失败[/bold red]\n"
|
|
544
|
+
f"请检查网络连接和仓库认证信息",
|
|
545
|
+
border_style="red"
|
|
546
|
+
))
|
|
547
|
+
cleanupTempFiles(work_dir, keep_temp, console)
|
|
548
|
+
sys.exit(1)
|
|
549
|
+
|
|
550
|
+
console.print("[green] ✓ 镜像推送成功[/green]")
|
|
551
|
+
|
|
552
|
+
# ===== 成功 =====
|
|
553
|
+
console.print(Panel.fit(
|
|
554
|
+
f"[bold green]✅ 构建成功![/bold green]\n"
|
|
555
|
+
f"镜像: {image_url}\n"
|
|
556
|
+
f"大小: {file_size_mb:.2f} MB\n"
|
|
557
|
+
f"切片数: {len(chunks)}",
|
|
558
|
+
border_style="green"
|
|
559
|
+
))
|
|
560
|
+
|
|
561
|
+
# 清理临时文件
|
|
562
|
+
cleanupTempFiles(work_dir, keep_temp, console)
|
|
563
|
+
|
|
564
|
+
except Exception as e:
|
|
565
|
+
console.print(Panel.fit(
|
|
566
|
+
f"[bold red]❌ 执行过程中出现异常[/bold red]\n"
|
|
567
|
+
f"错误: {str(e)}",
|
|
568
|
+
border_style="red"
|
|
569
|
+
))
|
|
570
|
+
if work_dir:
|
|
571
|
+
cleanupTempFiles(work_dir, keep_temp, console)
|
|
572
|
+
sys.exit(1)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
if __name__ == '__main__':
|
|
576
|
+
sifBuild()
|