conda_env_export_tool.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Conda环境导出工具
  5. 该脚本结合conda显式安装的包(包含版本信息)和pip显式安装的包,创建一个完整的environment.yml文件。
  6. 特性:
  7. - 📦 导出conda显式安装的包,包含精确的版本信息
  8. - 🐍 导出pip安装的包,保持完整的版本约束
  9. - ✨ 自动合并两种包管理器的依赖项
  10. - 🎯 只包含用户显式安装的包,避免冗余依赖
  11. 使用方法:
  12. python scripts/demos/conda_env_export_tool.py --env-name 环境名 --output environment.yml
  13. python scripts/demos/conda_env_export_tool.py -e base -o environment.yml
  14. 作者: AcidMap项目组
  15. """
  16. import argparse
  17. import subprocess
  18. import yaml
  19. import tempfile
  20. import os
  21. import json
  22. import re
  23. from pathlib import Path
  24. def run_command(command):
  25. """执行命令并返回输出"""
  26. try:
  27. result = subprocess.run(command, shell=True, capture_output=True, text=True, encoding='utf-8')
  28. if result.returncode != 0:
  29. print(f"命令执行失败: {command}")
  30. print(f"错误信息: {result.stderr}")
  31. return None
  32. return result.stdout
  33. except Exception as e:
  34. print(f"执行命令时出错: {e}")
  35. return None
  36. def get_conda_explicit_packages(env_name):
  37. """获取conda显式安装的包(包含版本信息)"""
  38. print(f"正在获取环境 '{env_name}' 的conda显式安装包...")
  39. with tempfile.NamedTemporaryFile(mode='w+', suffix='.yml', delete=False) as temp_file:
  40. temp_path = temp_file.name
  41. try:
  42. # 首先获取显式安装的包名(无版本)
  43. command = f"conda env export --name {env_name} --from-history --file {temp_path}"
  44. output = run_command(command)
  45. if output is None:
  46. return None
  47. # 读取显式安装的包名
  48. with open(temp_path, 'r', encoding='utf-8') as f:
  49. conda_env = yaml.safe_load(f)
  50. # 获取显式安装的包名列表
  51. explicit_packages = []
  52. if 'dependencies' in conda_env:
  53. for dep in conda_env['dependencies']:
  54. if isinstance(dep, str):
  55. # 使用更健壮的方法提取包名
  56. # 匹配包名(字母、数字、下划线、连字符、点)直到遇到版本操作符或特殊字符
  57. match = re.match(r'^([a-zA-Z0-9_.-]+)', dep.strip())
  58. if match:
  59. package_name = match.group(1)
  60. explicit_packages.append(package_name)
  61. else:
  62. print(f"警告: 无法解析包名 '{dep}',跳过")
  63. continue
  64. # 获取所有已安装包的详细信息
  65. command = f"conda list --name {env_name} --json"
  66. list_output = run_command(command)
  67. if list_output is None:
  68. print("警告: 无法获取包的详细信息,将返回无版本的包列表")
  69. return conda_env
  70. all_packages = json.loads(list_output)
  71. # 创建包名到版本的映射
  72. package_versions = {}
  73. for pkg in all_packages:
  74. if pkg.get('channel') != 'pypi': # 只处理conda包
  75. package_versions[pkg['name']] = f"{pkg['name']}={pkg['version']}"
  76. # 更新dependencies列表,添加版本信息
  77. versioned_dependencies = []
  78. for pkg_name in explicit_packages:
  79. if pkg_name in package_versions:
  80. versioned_dependencies.append(package_versions[pkg_name])
  81. else:
  82. print(f"警告: 未找到包 '{pkg_name}' 的版本信息,保持原样")
  83. versioned_dependencies.append(pkg_name)
  84. conda_env['dependencies'] = versioned_dependencies
  85. print(f"✅ 成功获取 {len(versioned_dependencies)} 个显式安装的conda包(包含版本信息)")
  86. return conda_env
  87. finally:
  88. # 清理临时文件
  89. if os.path.exists(temp_path):
  90. os.unlink(temp_path)
  91. def get_pip_packages(env_name):
  92. """获取pip安装的包"""
  93. print(f"正在获取环境 '{env_name}' 的pip安装包...")
  94. with tempfile.NamedTemporaryFile(mode='w+', suffix='.yml', delete=False) as temp_file:
  95. temp_path = temp_file.name
  96. try:
  97. # 导出完整环境(包含pip包)
  98. command = f"conda env export --name {env_name} --file {temp_path}"
  99. output = run_command(command)
  100. if output is None:
  101. return None
  102. # 读取生成的文件
  103. with open(temp_path, 'r', encoding='utf-8') as f:
  104. full_env = yaml.safe_load(f)
  105. # 提取pip包
  106. pip_packages = []
  107. if 'dependencies' in full_env:
  108. for dep in full_env['dependencies']:
  109. if isinstance(dep, dict) and 'pip' in dep:
  110. pip_packages = dep['pip']
  111. break
  112. return pip_packages
  113. finally:
  114. # 清理临时文件
  115. if os.path.exists(temp_path):
  116. os.unlink(temp_path)
  117. def create_combined_environment_file(env_name, output_file):
  118. """创建结合conda和pip包的环境文件"""
  119. # 获取conda显式包
  120. conda_env = get_conda_explicit_packages(env_name)
  121. if conda_env is None:
  122. print("获取conda包失败")
  123. return False
  124. # 获取pip包
  125. pip_packages = get_pip_packages(env_name)
  126. if pip_packages is None:
  127. print("获取pip包失败")
  128. return False
  129. # 合并
  130. if pip_packages:
  131. # 确保dependencies是列表
  132. if 'dependencies' not in conda_env:
  133. conda_env['dependencies'] = []
  134. # 添加pip包
  135. conda_env['dependencies'].append({'pip': pip_packages})
  136. # 检查文件是否已存在
  137. file_exists = os.path.exists(output_file)
  138. if file_exists:
  139. print(f"⚠️ 文件 '{output_file}' 已存在,将被替换")
  140. # 写入输出文件
  141. try:
  142. with open(output_file, 'w', encoding='utf-8') as f:
  143. yaml.dump(conda_env, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
  144. if file_exists:
  145. print(f"✅ 成功替换环境文件: {output_file}")
  146. else:
  147. print(f"✅ 成功创建环境文件: {output_file}")
  148. print(f"📦 Conda显式包数量: {len([dep for dep in conda_env['dependencies'] if isinstance(dep, str)])}")
  149. print(f"🐍 Pip包数量: {len(pip_packages) if pip_packages else 0}")
  150. return True
  151. except Exception as e:
  152. print(f"写入文件时出错: {e}")
  153. return False
  154. def main():
  155. """主函数"""
  156. parser = argparse.ArgumentParser(
  157. description="导出包含conda显式包(含版本)和pip包的环境文件",
  158. formatter_class=argparse.RawDescriptionHelpFormatter,
  159. epilog="""
  160. 示例:
  161. python scripts/demos/conda_env_export_tool.py --env-name GeoSys --output my_environment.yml
  162. python scripts/demos/conda_env_export_tool.py -e myenv -o environment.yml
  163. 注意: 该工具会自动获取显式安装包的版本信息,确保环境的精确重现。
  164. """
  165. )
  166. parser.add_argument(
  167. '--env-name', '-e',
  168. required=True,
  169. help="conda环境名称"
  170. )
  171. parser.add_argument(
  172. '--output', '-o',
  173. default='environment_combined.yml',
  174. help="输出文件名 (默认: environment_combined.yml)"
  175. )
  176. args = parser.parse_args()
  177. print(f"🚀 开始导出环境 '{args.env_name}' 到文件 '{args.output}'")
  178. print("=" * 50)
  179. success = create_combined_environment_file(args.env_name, args.output)
  180. if success:
  181. print("=" * 50)
  182. print("✨ 导出完成!")
  183. print(f"\n📝 使用此文件创建新环境:")
  184. print(f" conda env create -f {args.output}")
  185. else:
  186. print("❌ 导出失败")
  187. return 1
  188. return 0
  189. if __name__ == "__main__":
  190. exit(main())