123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- Conda环境导出工具
- 该脚本结合conda显式安装的包(包含版本信息)和pip显式安装的包,创建一个完整的environment.yml文件。
- 特性:
- - 📦 导出conda显式安装的包,包含精确的版本信息
- - 🐍 导出pip安装的包,保持完整的版本约束
- - ✨ 自动合并两种包管理器的依赖项
- - 🎯 只包含用户显式安装的包,避免冗余依赖
- 使用方法:
- python scripts/demos/conda_env_export_tool.py --env-name 环境名 --output environment.yml
- python scripts/demos/conda_env_export_tool.py -e base -o environment.yml
- 作者: AcidMap项目组
- """
- import argparse
- import subprocess
- import yaml
- import tempfile
- import os
- import json
- import re
- from pathlib import Path
- def run_command(command):
- """执行命令并返回输出"""
- try:
- result = subprocess.run(command, shell=True, capture_output=True, text=True, encoding='utf-8')
- if result.returncode != 0:
- print(f"命令执行失败: {command}")
- print(f"错误信息: {result.stderr}")
- return None
- return result.stdout
- except Exception as e:
- print(f"执行命令时出错: {e}")
- return None
- def get_conda_explicit_packages(env_name):
- """获取conda显式安装的包(包含版本信息)"""
- print(f"正在获取环境 '{env_name}' 的conda显式安装包...")
-
- with tempfile.NamedTemporaryFile(mode='w+', suffix='.yml', delete=False) as temp_file:
- temp_path = temp_file.name
-
- try:
- # 首先获取显式安装的包名(无版本)
- command = f"conda env export --name {env_name} --from-history --file {temp_path}"
- output = run_command(command)
-
- if output is None:
- return None
-
- # 读取显式安装的包名
- with open(temp_path, 'r', encoding='utf-8') as f:
- conda_env = yaml.safe_load(f)
-
- # 获取显式安装的包名列表
- explicit_packages = []
- if 'dependencies' in conda_env:
- for dep in conda_env['dependencies']:
- if isinstance(dep, str):
- # 使用更健壮的方法提取包名
- # 匹配包名(字母、数字、下划线、连字符、点)直到遇到版本操作符或特殊字符
- match = re.match(r'^([a-zA-Z0-9_.-]+)', dep.strip())
- if match:
- package_name = match.group(1)
- explicit_packages.append(package_name)
- else:
- print(f"警告: 无法解析包名 '{dep}',跳过")
- continue
-
- # 获取所有已安装包的详细信息
- command = f"conda list --name {env_name} --json"
- list_output = run_command(command)
-
- if list_output is None:
- print("警告: 无法获取包的详细信息,将返回无版本的包列表")
- return conda_env
-
- all_packages = json.loads(list_output)
-
- # 创建包名到版本的映射
- package_versions = {}
- for pkg in all_packages:
- if pkg.get('channel') != 'pypi': # 只处理conda包
- package_versions[pkg['name']] = f"{pkg['name']}={pkg['version']}"
-
- # 更新dependencies列表,添加版本信息
- versioned_dependencies = []
- for pkg_name in explicit_packages:
- if pkg_name in package_versions:
- versioned_dependencies.append(package_versions[pkg_name])
- else:
- print(f"警告: 未找到包 '{pkg_name}' 的版本信息,保持原样")
- versioned_dependencies.append(pkg_name)
-
- conda_env['dependencies'] = versioned_dependencies
- print(f"✅ 成功获取 {len(versioned_dependencies)} 个显式安装的conda包(包含版本信息)")
-
- return conda_env
-
- finally:
- # 清理临时文件
- if os.path.exists(temp_path):
- os.unlink(temp_path)
- def get_pip_packages(env_name):
- """获取pip安装的包"""
- print(f"正在获取环境 '{env_name}' 的pip安装包...")
-
- with tempfile.NamedTemporaryFile(mode='w+', suffix='.yml', delete=False) as temp_file:
- temp_path = temp_file.name
-
- try:
- # 导出完整环境(包含pip包)
- command = f"conda env export --name {env_name} --file {temp_path}"
- output = run_command(command)
-
- if output is None:
- return None
-
- # 读取生成的文件
- with open(temp_path, 'r', encoding='utf-8') as f:
- full_env = yaml.safe_load(f)
-
- # 提取pip包
- pip_packages = []
- if 'dependencies' in full_env:
- for dep in full_env['dependencies']:
- if isinstance(dep, dict) and 'pip' in dep:
- pip_packages = dep['pip']
- break
-
- return pip_packages
-
- finally:
- # 清理临时文件
- if os.path.exists(temp_path):
- os.unlink(temp_path)
- def create_combined_environment_file(env_name, output_file):
- """创建结合conda和pip包的环境文件"""
-
- # 获取conda显式包
- conda_env = get_conda_explicit_packages(env_name)
- if conda_env is None:
- print("获取conda包失败")
- return False
-
- # 获取pip包
- pip_packages = get_pip_packages(env_name)
- if pip_packages is None:
- print("获取pip包失败")
- return False
-
- # 合并
- if pip_packages:
- # 确保dependencies是列表
- if 'dependencies' not in conda_env:
- conda_env['dependencies'] = []
-
- # 添加pip包
- conda_env['dependencies'].append({'pip': pip_packages})
-
- # 检查文件是否已存在
- file_exists = os.path.exists(output_file)
- if file_exists:
- print(f"⚠️ 文件 '{output_file}' 已存在,将被替换")
-
- # 写入输出文件
- try:
- with open(output_file, 'w', encoding='utf-8') as f:
- yaml.dump(conda_env, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
-
- if file_exists:
- print(f"✅ 成功替换环境文件: {output_file}")
- else:
- print(f"✅ 成功创建环境文件: {output_file}")
- print(f"📦 Conda显式包数量: {len([dep for dep in conda_env['dependencies'] if isinstance(dep, str)])}")
- print(f"🐍 Pip包数量: {len(pip_packages) if pip_packages else 0}")
-
- return True
-
- except Exception as e:
- print(f"写入文件时出错: {e}")
- return False
- def main():
- """主函数"""
- parser = argparse.ArgumentParser(
- description="导出包含conda显式包(含版本)和pip包的环境文件",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
- 示例:
- python scripts/demos/conda_env_export_tool.py --env-name GeoSys --output my_environment.yml
- python scripts/demos/conda_env_export_tool.py -e myenv -o environment.yml
-
- 注意: 该工具会自动获取显式安装包的版本信息,确保环境的精确重现。
- """
- )
-
- parser.add_argument(
- '--env-name', '-e',
- required=True,
- help="conda环境名称"
- )
-
- parser.add_argument(
- '--output', '-o',
- default='environment_combined.yml',
- help="输出文件名 (默认: environment_combined.yml)"
- )
-
- args = parser.parse_args()
-
- print(f"🚀 开始导出环境 '{args.env_name}' 到文件 '{args.output}'")
- print("=" * 50)
-
- success = create_combined_environment_file(args.env_name, args.output)
-
- if success:
- print("=" * 50)
- print("✨ 导出完成!")
- print(f"\n📝 使用此文件创建新环境:")
- print(f" conda env create -f {args.output}")
- else:
- print("❌ 导出失败")
- return 1
-
- return 0
- if __name__ == "__main__":
- exit(main())
|