portal_deploy 0.2.0__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.
@@ -0,0 +1,24 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: portal_deploy
3
+ Version: 0.2.0
4
+ Summary: Ray environment deployment and building toolkit
5
+ Classifier: License :: OSI Approved :: MIT License
6
+ Classifier: Operating System :: OS Independent
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Requires-Python: >=3.10
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "portal_deploy"
3
+ version = "0.2.0" # 必须是未发布过的新版本
4
+ description = "Ray environment deployment and building toolkit"
5
+ long_description = { file = ["README.md"] } # 兼容所有版本的写法
6
+ long_description_content_type = "text/markdown"
7
+ requires-python = ">=3.10"
8
+ classifiers = [
9
+ "Programming Language :: Python :: 3.10",
10
+ "License :: OSI Approved :: MIT License",
11
+ "Operating System :: OS Independent",
12
+ ]
13
+
14
+ # 关键修正:用 [project.scripts] 作为配置段标题(方括号),下面配具体命令
15
+ [project.scripts]
16
+ # 命令名 = "包名.模块名:函数名"
17
+ raydeploy-build = "raykit.env_builder:main"
18
+ raydeploy-install = "raykit.env_installer:main"
19
+
20
+ [build-system]
21
+ requires = ["hatchling>=1.21.0"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.build]
25
+ include = ["raykit/"]
26
+ exclude = [".idea/", ".venv/", "*.pth", "*.jpg", "uv.lock"]
@@ -0,0 +1,4 @@
1
+ # raykit/__init__.py
2
+ from .env_builder import main as build
3
+ # 如果 env_installer.py 中的函数是 install_env,就这样导入
4
+ from .env_installer import main as install
@@ -0,0 +1,22 @@
1
+ # raykit/cli.py
2
+ import argparse
3
+ from raykit import env_builder
4
+ from raykit import env_installer
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(prog="raydeploy")
8
+ subparsers = parser.add_subparsers(dest="cmd", required=True)
9
+
10
+ # 子命令:build
11
+ subparsers.add_parser("build", help="Build the environment")
12
+ # 子命令:install
13
+ subparsers.add_parser("install", help="Install the environment")
14
+
15
+ args = parser.parse_args()
16
+ if args.cmd == "build":
17
+ env_builder.main()
18
+ elif args.cmd == "install":
19
+ env_installer.main()
20
+
21
+ if __name__ == "__main__":
22
+ main()
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 环境构建SDK工具:固定Python 3.10.19,生成uv标准的pyproject.toml源配置
5
+ """
6
+ import os
7
+ import sys
8
+ from typing import Dict, List, Tuple
9
+
10
+ # 固定Python版本(核心配置)
11
+ FIXED_PYTHON_VERSION = "3.10.19"
12
+ # 兼容的Python版本范围(用于提示)
13
+ COMPATIBLE_PYTHON_RANGE = ">=3.10,<3.11"
14
+
15
+ # 预定义框架版本映射(新增uv源名称/url映射)
16
+ FRAMEWORK_VERSIONS = {
17
+ "pytorch": {
18
+ "2.2.1": {
19
+ "cpu": {
20
+ "deps": {
21
+ "torch": "2.2.1",
22
+ "torchvision": "0.17.1",
23
+ "torchaudio": "2.2.1"
24
+ },
25
+ "install_cmd": "pip install torch==2.2.1+cpu torchvision==0.17.1+cpu torchaudio==2.2.1+cpu -f https://download.pytorch.org/whl/cpu/torch_stable.html",
26
+ "uv_index_name": "pytorch-official",
27
+ "uv_index_url": "https://download.pytorch.org/whl/cpu",
28
+ "cuda_alias": "cpu"
29
+ },
30
+ "gpu": {
31
+ "cuda_version": "12.1",
32
+ "deps": {
33
+ "torch": "2.2.1",
34
+ "torchvision": "0.17.1",
35
+ "torchaudio": "2.2.1"
36
+ },
37
+ "install_cmd": "pip install torch==2.2.1 torchvision==0.17.1 torchaudio==2.2.1 --index-url https://download.pytorch.org/whl/cu121",
38
+ "uv_index_name": "pytorch-official",
39
+ "uv_index_url": "https://download.pytorch.org/whl/cu121",
40
+ "cuda_alias": "cu121"
41
+ }
42
+ },
43
+ "2.5.1": {
44
+ "cpu": {
45
+ "deps": {
46
+ "torch": "2.5.1",
47
+ "torchvision": "0.20.1",
48
+ "torchaudio": "2.5.1"
49
+ },
50
+ "install_cmd": "pip install torch==2.5.1+cpu torchvision==0.20.1+cpu torchaudio==2.5.1+cpu -f https://download.pytorch.org/whl/cpu/torch_stable.html",
51
+ "uv_index_name": "pytorch-official",
52
+ "uv_index_url": "https://download.pytorch.org/whl/cpu",
53
+ "cuda_alias": "cpu"
54
+ },
55
+ "gpu": {
56
+ "cuda_version": "12.1",
57
+ "deps": {
58
+ "torch": "2.5.1",
59
+ "torchvision": "0.20.1",
60
+ "torchaudio": "2.5.1"
61
+ },
62
+ "install_cmd": "conda install pytorch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 pytorch-cuda=12.1 -c pytorch -c nvidia",
63
+ "uv_index_name": "pytorch-official",
64
+ "uv_index_url": "https://download.pytorch.org/whl/cu121",
65
+ "cuda_alias": "cu121",
66
+ "conda_channels": ["pytorch", "nvidia"]
67
+ }
68
+ }
69
+ },
70
+ "paddle": {
71
+ "2.5.2": {
72
+ "cpu": {
73
+ "deps": {
74
+ "paddlepaddle": "2.5.2",
75
+ "paddlenlp": "2.5.2"
76
+ },
77
+ "install_cmd": "pip install paddlepaddle==2.5.2 -i https://pypi.tuna.tsinghua.edu.cn/simple",
78
+ "uv_index_name": "paddle-official",
79
+ "uv_index_url": "https://pypi.tuna.tsinghua.edu.cn/simple",
80
+ "cuda_alias": "cpu"
81
+ },
82
+ "gpu": {
83
+ "cuda_version": "11.7",
84
+ "deps": {
85
+ "paddlepaddle-gpu": "2.5.2",
86
+ "paddlenlp": "2.5.2"
87
+ },
88
+ "install_cmd": "pip install paddlepaddle-gpu==2.5.2.post117 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html",
89
+ "uv_index_name": "paddle-official",
90
+ "uv_index_url": "https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html",
91
+ "cuda_alias": "cu117"
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ # 强制内置依赖
98
+ MANDATORY_DEPS = {
99
+ "ray[serve]": "2.54.0"
100
+ }
101
+
102
+
103
+ class EnvBuilderSDK:
104
+ def __init__(self):
105
+ self.selected_framework = None
106
+ self.selected_version = None
107
+ self.selected_device = None # cpu/gpu
108
+ self.framework_deps = {}
109
+ self.requirements_deps = {}
110
+ self.all_deps = {}
111
+ self.install_config = {} # 存储uv源配置、conda通道等
112
+
113
+ def check_python_version(self):
114
+ """检查当前Python版本,给出兼容性提示"""
115
+ current_ver = ".".join(map(str, sys.version_info[:3]))
116
+ print(f"\n📌 本工具要求Python版本:{FIXED_PYTHON_VERSION}")
117
+ print(f" 当前Python版本:{current_ver}")
118
+ if current_ver != FIXED_PYTHON_VERSION:
119
+ print(f"⚠️ 版本不匹配!建议使用conda创建指定版本环境:")
120
+ print(f" conda create -n farmland python={FIXED_PYTHON_VERSION}")
121
+ print(f" conda activate farmland")
122
+ else:
123
+ print("✅ Python版本符合要求!")
124
+
125
+ def select_framework(self) -> str:
126
+ """交互式选择框架"""
127
+ print("\n===== 选择深度学习框架 =====")
128
+ frameworks = list(FRAMEWORK_VERSIONS.keys())
129
+ for idx, fw in enumerate(frameworks, 1):
130
+ print(f"{idx}. {fw.upper()}")
131
+
132
+ while True:
133
+ try:
134
+ choice = int(input(f"\n请输入框架序号(1-{len(frameworks)}): "))
135
+ if 1 <= choice <= len(frameworks):
136
+ self.selected_framework = frameworks[choice - 1]
137
+ print(f"已选择框架:{self.selected_framework.upper()}")
138
+ return self.selected_framework
139
+ else:
140
+ print(f"请输入1-{len(frameworks)}之间的数字!")
141
+ except ValueError:
142
+ print("请输入有效的数字!")
143
+
144
+ def select_device_type(self) -> str:
145
+ """选择CPU/GPU版本"""
146
+ print(f"\n===== 选择{self.selected_framework.upper()}运行设备 =====")
147
+ devices = ["cpu", "gpu"]
148
+ for idx, dev in enumerate(devices, 1):
149
+ print(f"{idx}. {dev.upper()}")
150
+
151
+ while True:
152
+ try:
153
+ choice = int(input(f"\n请输入设备序号(1-{len(devices)}): "))
154
+ if 1 <= choice <= len(devices):
155
+ self.selected_device = devices[choice - 1]
156
+ print(f"已选择设备:{self.selected_device.upper()}")
157
+ # 提示CUDA版本(GPU时)
158
+ if self.selected_device == "gpu":
159
+ cuda_ver = FRAMEWORK_VERSIONS[self.selected_framework][self.selected_version][
160
+ self.selected_device].get("cuda_version")
161
+ if cuda_ver:
162
+ print(f"⚠️ 该版本需要CUDA {cuda_ver},请确保系统已安装对应版本CUDA")
163
+ return self.selected_device
164
+ else:
165
+ print(f"请输入1-{len(devices)}之间的数字!")
166
+ except ValueError:
167
+ print("请输入有效的数字!")
168
+
169
+ def select_framework_version(self) -> str:
170
+ """交互式选择框架版本(先选版本,再选CPU/GPU)"""
171
+ print(f"\n===== 选择{self.selected_framework.upper()}版本 =====")
172
+ versions = list(FRAMEWORK_VERSIONS[self.selected_framework].keys())
173
+ for idx, ver in enumerate(versions, 1):
174
+ print(f"{idx}. 版本{ver}")
175
+
176
+ while True:
177
+ try:
178
+ choice = int(input(f"\n请输入版本序号(1-{len(versions)}): "))
179
+ if 1 <= choice <= len(versions):
180
+ self.selected_version = versions[choice - 1]
181
+ print(f"已选择{self.selected_framework.upper()}版本:{self.selected_version}")
182
+
183
+ # 选择CPU/GPU
184
+ self.select_device_type()
185
+
186
+ # 获取该版本+设备的依赖和安装配置
187
+ self.framework_deps = \
188
+ FRAMEWORK_VERSIONS[self.selected_framework][self.selected_version][self.selected_device]["deps"]
189
+ self.install_config = FRAMEWORK_VERSIONS[self.selected_framework][self.selected_version][
190
+ self.selected_device]
191
+
192
+ # 打印安装命令参考
193
+ print(f"\n📝 参考安装命令:{self.install_config['install_cmd']}")
194
+ return self.selected_version
195
+ else:
196
+ print(f"请输入1-{len(versions)}之间的数字!")
197
+ except ValueError:
198
+ print("请输入有效的数字!")
199
+
200
+ def read_requirements(self, file_path: str = "requirements.txt") -> Dict[str, str]:
201
+ """读取requirements.txt文件,解析依赖"""
202
+ self.requirements_deps = {}
203
+ if not os.path.exists(file_path):
204
+ print(f"\n⚠️ 未找到{file_path}文件,跳过读取第三方依赖")
205
+ return self.requirements_deps
206
+
207
+ print(f"\n===== 读取{file_path}依赖 =====")
208
+ with open(file_path, "r", encoding="utf-8") as f:
209
+ lines = f.readlines()
210
+
211
+ for line in lines:
212
+ line = line.strip()
213
+ # 跳过注释和空行
214
+ if not line or line.startswith("#"):
215
+ continue
216
+ # 解析包名和版本(支持==/>=/<=等)
217
+ if "==" in line:
218
+ pkg, ver = line.split("==", 1)
219
+ self.requirements_deps[pkg.strip()] = ver.strip()
220
+ else:
221
+ # 无版本号时保留原字符串
222
+ self.requirements_deps[line.strip()] = ""
223
+
224
+ print(f"成功读取{len(self.requirements_deps)}个依赖:{list(self.requirements_deps.keys())}")
225
+ return self.requirements_deps
226
+
227
+ def merge_dependencies(self) -> Dict[str, str]:
228
+ """合并所有依赖:框架依赖 + requirements依赖 + 强制依赖"""
229
+ self.all_deps = {}
230
+ # 1. 框架依赖(优先级最高,避免版本冲突)
231
+ self.all_deps.update(self.framework_deps)
232
+ # 2. requirements依赖(若与框架依赖冲突,以框架为准)
233
+ for pkg, ver in self.requirements_deps.items():
234
+ if pkg not in self.all_deps:
235
+ self.all_deps[pkg] = ver
236
+ # 3. 强制依赖(ray[serve],优先级最高)
237
+ self.all_deps.update(MANDATORY_DEPS)
238
+
239
+ print(f"\n===== 合并后总依赖 =====")
240
+ for pkg, ver in self.all_deps.items():
241
+ print(f"{pkg}: {ver if ver else '未指定版本'}")
242
+ return self.all_deps
243
+
244
+ def generate_pyproject_toml(self, output_path: str = "pyproject.toml") -> None:
245
+ """生成pyproject.toml文件(固定Python 3.10.19 + uv源配置)"""
246
+ # 基础配置
247
+ pyproject_content = f"""
248
+ [project]
249
+ name = "tool"
250
+ version = "0.1.0"
251
+ description = "(Python {FIXED_PYTHON_VERSION} | {self.selected_framework.upper()} {self.selected_version} {self.selected_device.upper()})"
252
+ authors = [{{ name="Your Name", email="your@email.com" }}]
253
+ requires-python = "{COMPATIBLE_PYTHON_RANGE}" # 强制Python 3.10.x,精确版本{FIXED_PYTHON_VERSION}
254
+ license = {{ file="LICENSE" }}
255
+
256
+ # 核心依赖(包含{self.selected_framework} {self.selected_device}版本 + 第三方依赖)
257
+ dependencies = [
258
+ """
259
+ # 添加所有依赖到dependencies列表
260
+ dep_lines = []
261
+ for pkg, ver in self.all_deps.items():
262
+ if ver:
263
+ dep_lines.append(f' "{pkg}=={ver}",')
264
+ else:
265
+ dep_lines.append(f' "{pkg}",')
266
+
267
+ pyproject_content += "\n".join(dep_lines)
268
+ pyproject_content += """
269
+ ]
270
+
271
+ """
272
+ # ========== uv标准的源配置 ==========
273
+ pyproject_content += f"""# 新增 {self.selected_framework} 官方源({self.install_config['cuda_alias']} 版本)
274
+ [[tool.uv.index]]
275
+ name = "{self.install_config['uv_index_name']}"
276
+ url = "{self.install_config['uv_index_url']}"
277
+ explicit = true
278
+
279
+ """
280
+ # 针对PyTorch:强制torch/torchaudio/torchvision从官方源下载
281
+ if self.selected_framework == "pytorch":
282
+ pyproject_content += f"""# 关键:指定PyTorch相关包强制从 {self.install_config['uv_index_name']} 源下载
283
+ [tool.uv.sources]
284
+ torch = {{ index = "{self.install_config['uv_index_name']}" }}
285
+ torchaudio = {{ index = "{self.install_config['uv_index_name']}" }}
286
+ torchvision = {{ index = "{self.install_config['uv_index_name']}" }}
287
+
288
+ """
289
+ # 针对Paddle:强制Paddle相关包从官方源下载
290
+ elif self.selected_framework == "paddle":
291
+ pyproject_content += f"""# 关键:指定Paddle相关包强制从 {self.install_config['uv_index_name']} 源下载
292
+ [tool.uv.sources]
293
+ paddlepaddle = {{ index = "{self.install_config['uv_index_name']}" }}
294
+ paddlepaddle-gpu = {{ index = "{self.install_config['uv_index_name']}" }}
295
+ paddlenlp = {{ index = "{self.install_config['uv_index_name']}" }}
296
+
297
+ """
298
+ # 可选开发依赖
299
+ pyproject_content += """[project.optional-dependencies]
300
+ dev = [
301
+ "pytest>=7.0",
302
+ "black>=23.0",
303
+ "flake8>=6.0"
304
+ ]
305
+ """
306
+
307
+ # 写入文件
308
+ with open(output_path, "w", encoding="utf-8") as f:
309
+ f.write(pyproject_content)
310
+
311
+ print(f"\n✅ 成功生成{output_path}文件!")
312
+ print(f"文件路径:{os.path.abspath(output_path)}")
313
+ # 提示关键信息
314
+ print(f"📌 强制指定Python版本:{FIXED_PYTHON_VERSION}")
315
+ if self.selected_device == "gpu":
316
+ cuda_ver = self.install_config.get("cuda_version")
317
+ print(f"⚠️ 注意:该配置需要CUDA {cuda_ver},请确保系统已安装对应版本CUDA")
318
+
319
+ def run(self):
320
+ """SDK主执行流程"""
321
+ print("===== 环境构建SDK(Python 3.10.19固定版)=====")
322
+ # 前置步骤:检查Python版本
323
+ self.check_python_version()
324
+ # 步骤1:选择框架
325
+ self.select_framework()
326
+ # 步骤2:选择框架版本 + CPU/GPU
327
+ self.select_framework_version()
328
+ # 步骤3:读取requirements.txt
329
+ self.read_requirements()
330
+ # 步骤4:合并依赖
331
+ self.merge_dependencies()
332
+ # 步骤5:生成pyproject.toml
333
+ self.generate_pyproject_toml()
334
+ print("\n===== SDK执行完成 =====")
335
+
336
+ # 核心新增:定义main函数,供raykit.build调用
337
+ def main():
338
+ """环境构建的入口函数(供模块导入调用)"""
339
+ sdk = EnvBuilderSDK()
340
+ sdk.run()
341
+
342
+ # 保留原有直接运行的逻辑
343
+ if __name__ == "__main__":
344
+ main()
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Portal 环境构建脚本(全平台兼容:Windows/Linux)
5
+ 特性:
6
+ 1. 自动识别系统类型,适配 Conda 执行逻辑
7
+ 2. 智能跳过已完成步骤(环境/uv/依赖)
8
+ 3. 自动处理 uv.lock 锁文件(首次不加 --frozen)
9
+ 4. 兼容 Conda 初始化、路径分隔符、权限等问题
10
+ """
11
+
12
+ import subprocess
13
+ import sys
14
+ import argparse
15
+ import os
16
+ import re
17
+ import platform
18
+
19
+ # 跨平台彩色输出兼容
20
+ try:
21
+ from colorama import init, Fore, Style
22
+
23
+ # Windows 初始化 colorama,Linux 无需
24
+ if platform.system() == "Windows":
25
+ init(autoreset=True)
26
+ except ImportError:
27
+ # 无 colorama 时降级为无颜色输出
28
+ class FakeColor:
29
+ def __getattr__(self, name):
30
+ return ""
31
+
32
+
33
+ Fore = FakeColor()
34
+ Style = FakeColor()
35
+
36
+
37
+ class EnvBuilderError(Exception):
38
+ """自定义异常类"""
39
+ pass
40
+
41
+
42
+ def get_conda_init_cmd():
43
+ """获取对应系统的 Conda 初始化命令"""
44
+ system = platform.system()
45
+ if system == "Linux":
46
+ # Linux 下 Conda 初始化路径(兼容 Miniconda/Anaconda)
47
+ conda_paths = [
48
+ "~/miniconda3/etc/profile.d/conda.sh",
49
+ "~/anaconda3/etc/profile.d/conda.sh",
50
+ "/opt/conda/etc/profile.d/conda.sh"
51
+ ]
52
+ for path in conda_paths:
53
+ abs_path = os.path.expanduser(path)
54
+ if os.path.exists(abs_path):
55
+ return f"source {abs_path} && "
56
+ return "source ~/miniconda3/etc/profile.d/conda.sh && " # 默认路径
57
+ return "" # Windows 无需初始化
58
+
59
+
60
+ def run_command(cmd, description, check_only=False):
61
+ """
62
+ 执行系统命令(全平台兼容)
63
+ check_only=True: 仅检查结果,不抛出异常、不打印冗余输出
64
+ """
65
+ if not check_only:
66
+ print(f"{Fore.BLUE}[*] {description}...{Style.RESET_ALL}")
67
+
68
+ system = platform.system()
69
+ try:
70
+ # 构建最终执行的命令
71
+ final_cmd = cmd
72
+ if system == "Linux" and cmd[0] == "conda":
73
+ # Linux 下:封装 Conda 命令,确保初始化 + bash 执行
74
+ conda_init = get_conda_init_cmd()
75
+ cmd_str = conda_init + " ".join(cmd)
76
+ final_cmd = ["bash", "-c", cmd_str]
77
+ shell = False
78
+ else:
79
+ # Windows 下:启用 shell 以支持 conda 命令
80
+ shell = system == "Windows"
81
+
82
+ # 执行命令
83
+ result = subprocess.run(
84
+ final_cmd,
85
+ stdout=subprocess.PIPE,
86
+ stderr=subprocess.STDOUT,
87
+ text=True,
88
+ encoding="utf-8",
89
+ errors="ignore",
90
+ shell=shell
91
+ )
92
+
93
+ # 打印输出(非检查模式)
94
+ if not check_only and result.stdout:
95
+ print(f"{Fore.GREEN}[+] 输出: {result.stdout}{Style.RESET_ALL}")
96
+
97
+ # 返回执行结果
98
+ return result.returncode == 0, result.stdout
99
+
100
+ except Exception as e:
101
+ if check_only:
102
+ return False, str(e)
103
+ raise EnvBuilderError(f"{Fore.RED}[-] 执行{description}出错: {str(e)}{Style.RESET_ALL}")
104
+
105
+
106
+ def check_conda_env_exists(env_name):
107
+ """检查 Conda 环境是否存在(跨平台)"""
108
+ print(f"{Fore.BLUE}[*] 检查虚拟环境 {env_name} 是否存在...{Style.RESET_ALL}")
109
+ success, output = run_command(
110
+ ["conda", "info", "--envs"],
111
+ f"检查 Conda 环境 {env_name}",
112
+ check_only=True
113
+ )
114
+ if success:
115
+ env_pattern = re.compile(rf"{env_name}\s+")
116
+ if env_pattern.search(output):
117
+ print(f"{Fore.GREEN}[+] 虚拟环境 {env_name} 已存在,跳过创建步骤{Style.RESET_ALL}")
118
+ return True
119
+ print(f"{Fore.YELLOW}[!] 虚拟环境 {env_name} 不存在,需要创建{Style.RESET_ALL}")
120
+ return False
121
+
122
+
123
+ def check_uv_installed(env_name):
124
+ """检查指定 Conda 环境中是否安装 uv(跨平台)"""
125
+ print(f"{Fore.BLUE}[*] 检查环境 {env_name} 中是否安装 uv...{Style.RESET_ALL}")
126
+ cmd = ["conda", "run", "-n", env_name, "uv", "--version"]
127
+ success, output = run_command(cmd, "检查 uv 安装状态", check_only=True)
128
+ if success and "uv " in output: # 确保输出包含 uv 版本信息(避免误判)
129
+ print(f"{Fore.GREEN}[+] uv 已安装,跳过安装步骤{Style.RESET_ALL}")
130
+ return True
131
+ print(f"{Fore.YELLOW}[!] uv 未安装,需要安装{Style.RESET_ALL}")
132
+ return False
133
+
134
+
135
+ def check_deps_synced(env_name):
136
+ """检查依赖是否已还原(跨平台)"""
137
+ print(f"{Fore.BLUE}[*] 检查依赖是否已还原...{Style.RESET_ALL}")
138
+ # 检查 uv.lock 是否存在 + 依赖是否安装
139
+ if os.path.exists("uv.lock"):
140
+ success, _ = run_command(
141
+ ["conda", "run", "-n", env_name, "uv", "list"],
142
+ "检查依赖列表",
143
+ check_only=True
144
+ )
145
+ if success:
146
+ print(f"{Fore.GREEN}[+] 依赖已还原,跳过 uv sync 步骤{Style.RESET_ALL}")
147
+ return True
148
+ print(f"{Fore.YELLOW}[!] 依赖未还原,需要执行 uv sync{Style.RESET_ALL}")
149
+ return False
150
+
151
+
152
+ def create_portal_env(env_name="portal_test", python_version="3.10.19"):
153
+ """核心功能:智能创建并配置测试环境(全平台)"""
154
+ try:
155
+ # 1. 检查 Conda 是否可用
156
+ conda_ok, conda_output = run_command(["conda", "--version"], "检查 Conda 安装状态", check_only=True)
157
+ if not conda_ok:
158
+ hint = ""
159
+ if platform.system() == "Linux":
160
+ hint = "\n 请先安装 Conda,或执行: source ~/miniconda3/etc/profile.d/conda.sh"
161
+ elif platform.system() == "Windows":
162
+ hint = "\n 请确保 Conda 已添加到系统 PATH 中"
163
+ raise EnvBuilderError(f"{Fore.RED}未检测到 Conda!{hint}{Style.RESET_ALL}")
164
+
165
+ # 2. 检查并创建虚拟环境
166
+ env_exists = check_conda_env_exists(env_name)
167
+ if not env_exists:
168
+ run_command(
169
+ ["conda", "create", "-n", env_name, f"python={python_version}", "-y"],
170
+ f"创建虚拟环境 {env_name} (Python {python_version})"
171
+ )
172
+
173
+ # 3. 检查并安装 uv(Linux 加 --user 避免权限问题)
174
+ uv_installed = check_uv_installed(env_name)
175
+ if not uv_installed:
176
+ install_cmd = ["conda", "run", "-n", env_name, "pip", "install", "uv"]
177
+ if platform.system() == "Linux":
178
+ install_cmd.append("--user")
179
+ run_command(install_cmd, "安装 uv 包管理器")
180
+
181
+ # 4. 检查 pyproject.toml 是否存在
182
+ if not os.path.exists("pyproject.toml"):
183
+ raise EnvBuilderError(f"{Fore.RED}当前目录未找到 pyproject.toml 文件!{Style.RESET_ALL}")
184
+
185
+ # 5. 检查并执行 uv sync(跨平台适配)
186
+ deps_synced = check_deps_synced(env_name)
187
+ if not deps_synced:
188
+ uv_sync_cmd = ["conda", "run", "-n", env_name, "uv", "sync"]
189
+ # Linux 下强制指定 Python 解释器路径(动态拼接)
190
+ if platform.system() == "Linux":
191
+ # 自动获取 conda 环境路径
192
+ conda_env_path = os.path.expanduser(f"~/miniconda3/envs/{env_name}/bin/python")
193
+ uv_sync_cmd.extend(["--python", conda_env_path])
194
+ run_command(uv_sync_cmd, "还原线上环境依赖")
195
+
196
+ # 执行完成提示(区分系统)
197
+ print(f"\n{Fore.GREEN}[✓] 环境检查/构建完成!{Style.RESET_ALL}")
198
+ activate_hint = ""
199
+ if platform.system() == "Linux":
200
+ activate_hint = f"source activate {env_name}"
201
+ elif platform.system() == "Windows":
202
+ activate_hint = f"conda activate {env_name}"
203
+ print(f"{Fore.YELLOW}[提示] 手动激活环境:{activate_hint}{Style.RESET_ALL}")
204
+
205
+ except EnvBuilderError as e:
206
+ print(f"\n{Fore.RED}[✗] 环境构建失败:{str(e)}{Style.RESET_ALL}")
207
+ sys.exit(1)
208
+ except KeyboardInterrupt:
209
+ print(f"\n{Fore.YELLOW}[!] 用户中断操作{Style.RESET_ALL}")
210
+ sys.exit(0)
211
+
212
+
213
+ def main():
214
+ """命令行参数解析和入口(全平台)"""
215
+ parser = argparse.ArgumentParser(
216
+ description="Portal 测试环境一键构建工具(Windows/Linux 通用)",
217
+ formatter_class=argparse.RawDescriptionHelpFormatter,
218
+ epilog="示例:\n python env_builder.py\n python env_builder.py --env-name my_env --python 3.10.20"
219
+ )
220
+ parser.add_argument(
221
+ "--env-name",
222
+ default="portal_test",
223
+ help="虚拟环境名称(默认:portal_test)"
224
+ )
225
+ parser.add_argument(
226
+ "--python",
227
+ default="3.10.19",
228
+ help="Python 版本(默认:3.10.19)"
229
+ )
230
+
231
+ args = parser.parse_args()
232
+
233
+ # 检查 Python 版本
234
+ if sys.version_info < (3, 10):
235
+ print(f"{Fore.RED}[-] 该脚本需要 Python 3.10 及以上版本!{Style.RESET_ALL}")
236
+ sys.exit(1)
237
+
238
+ create_portal_env(env_name=args.env_name, python_version=args.python)
239
+
240
+
241
+ if __name__ == "__main__":
242
+ main()