浅谈编程环境管理

做开发免不了要搭建开发环境,也免不了安装同一编程平台或编程工具的不同版本。由此也产生了许多与版本相关的环境和配置问题。与环境斗,可谓苦不堪言。与环境斗,我们必须百折不挠。本文记录和总结了我用过的那些环境管理器以及一些典型问题。

编程环境管理有时是个小烦恼,有时又颇为棘手。例如,我已经能够在 Android Studio (以下简称 AS) 中成功编译项目,但当在命令行中操作时却仍然提示找不到 JDK。

1
2
3
MacBook-Pro Anki-Android % ./gradlew
The operation couldn’t be completed. Unable to locate a Java Runtime.
Please visit http://www.java.com for information on installing Java.

这个问题很容易解释,因为我的环境变量中确实不包含可运行的 java 命令,所以无法运行 Java 程序是正常的。AS 能正常构建是因为它内置了一份 JDK。然而,通过这个案例,我们仍能看到编程环境管理确实是一个或大或小的麻烦。

不过,有问题自然就有办法。几乎所有主流的编程语言或环境都有相应的环境管理器(environment manager)。常见的包括:

  • Java - jenv
  • Python - pyenv 和 venv
  • Node - nvm
  • Flutter - fvm

Java

临时配置 JDK 环境

针对刚才提到 AS 下命令行中找不到 JDK 的问题,我们可以自己手动安装一个 JDK 并正确配置环境变量。另一个简单的解决办法是找到 AS 内置 Gradle 自带 JDK 的路径,手动配置到环境变量中。

-w1050

1
2
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
export PATH=$PATH:$JAVA_HOME

配置成功后,Gradle 即可正常运行。
-w1251

这个办法的好处是 gradlew 命令使用 AS 内置的 JDK,不同的构建方式不存在任何版本兼容风险。

手动配置多版本 JDK 环境

Mac安装多版本JDK - 知乎 中有提及如何手动配置多版本 JDK。

1
2
3
4
5
6
7
8
9
10
# 配置多版本 JDK
export JAVA_11_HOME=...
export JAVA_17_HOME=...

# 配置默认 JDK
export JAVA_HOME=...

# 通过别名切换 JDK
alias jdk11="export JAVA_HOME=$JAVA_11_HOME"
alias jdk17="export JAVA_HOME=$JAVA_17_HOME"

配置完成后就可以通过 java -versionjdk<version> 来查看和切换 JDK 版本。

通过 jenv 配置多版本 JDK 环境

我们也可以借助 jenv 这个工具来配置多版本 JDK 环境。jenv 通过设置环境变量来管理不同的 JDK 版本。当你使用 jenv 安装多个 JDK 版本时,它会在你的系统中创建一个 .jenv 目录,其中包含指向各个 JDK 版本的符号链接。jenv 会在项目目录下创建 .java-version 文件,并在恰当的时机修改环境变量以使用指定的 Java 版本。

  • 安装 jenv
    • 通过 brew install jenv 即可在 Mac 上安装 jenv
  • 配置 jenv。为了保证 jenv 在 shell 中生效,有两个细节要特别注意:
    • jenv 初始化。命令行中使用 jenv 前是务必要初始化!
    • 开启 jenv export 插件。开启这个插件后才能让 jenv 管理 JAVA_HOME 环境变量!

另外,jenv 本身并不负责安装 JDK!(原文:jenv does not, by itself, install Java)

那如何简单快速地安装 JDK 呢?

第一种办法,如果机器上已装好 AS,我们可以借助 AS 来安装 JDK。AS > Build > Build Tools > Gradle 中,点击 Gradle JDK 右侧下拉菜单

-w980

-w973

第二种办法,通过 brew 来安装 JDK。

1
2
brew search jdk
brew install openjdk@17

Python

我儿子在我的 MacBook 上玩乌龟编程时,他遇到一个非常诡异的问题。以下是他的代码:

1
2
3
4
5
6
7
8
9
10
11
12
from turtle import *
def circle (pensize2,size,colour):
pensize(pensize2)

fillcolor(1.0, 0, 0)
begin_fill()

for i in range(360):
fd(size)
rt(1)
end_fill()
circle(5,2,'red')

按理来说,这段代码会绘制一个圆形,但实际上却显示一个闪烁的黑色界面。搜索一番,发现问题原因是 Python 3.9.6 的 tinker 模块在 M2 上存在某个 bug。新版本中已修复该 bug。

我的 MacBook 上装有 3.9.6 和 3.12.1 两个版本的 Python。于是我用 venv 创建并切换切换到一个虚拟的 Python 3.12.1,再次运行代码后正常画出一个圆形。

-w1911

在以上操作过程中,使用 pyenv 和 venv 切换 Python 版本/环境的过程几乎无成本,不过对于排查和定位问题来说却意义重大。

对于 Python 初学者来说,有时不容易理解 pyenv 和 venv 之间的区别。pyenv 和 venv 用于解决不同的问题,因此了解它们的差异对于有效地管理 Python 环境至关重要。

Python 初学者有时不容易理解 pyenv 和 venv 之间的区别。pyenv 和 venv 用于解决不同的问题,

官方的关键描述如下:

pyenv lets you easily switch between multiple versions of Python

The venv module supports creating lightweight “virtual environments”

我们归纳一下:

  • pyenv
    • 目的:管理系统范围内的 Python 版本(base environments,实体环境)。
    • 特点:
      • 允许在同一系统上安装和使用多个 Python 版本。
      • 便于在不同 Python 版本之间切换。
  • venv
    • 目的:为特定项目或应用程序创建隔离的 Python 环境(virtual environments,虚拟环境)。
    • 特点:
      • 隔离项目依赖项,防止冲突。
      • 允许在不同项目中使用不同的 Python 版本。
      • 仅影响当前项目目录。
      • 需要手动激活和停用环境。

要管理多版本 Python 实体环境(base environments)好理解,因为类似的需求在 Java 和 Node 中也存在。比如说,同时开发两个 Java 项目,一个要用 Java 11,另一个要用 Java 17,那必然要同时安装两个版本的 JDK。

那 venv 存在的意义是什么?官网是这样描述的。

The venv module supports creating lightweight “virtual environments”, each with their own independent set of Python packages installed in their site directories. A virtual environment is created on top of an existing Python installation, known as the virtual environment’s “base” Python

你现在是不是懂了?可能是的。venv 用于隔离不同项目,方便管理不同项目的 Python 版本及对应的依赖。

那请思考一个问题,为什么 Java 和 Node 并不需要”虚拟”环境?本文总结中我会再次讨论这个问题。

pyenv 安装与使用

这里并不介绍 pyenv 的安装和使用方法,而是记录我曾经遇到并且今后还可能会遇到的两个典型小问题(问题虽小,不能快速解决的话,经常误事。希望下次出问题时我不是谷歌搜索答案而是翻自己的博客):

  • 无法安装指定版本的 Python
  • 无法找到已安装的 Python

第一个问题比较容易解释。 pyenv 是从源码编译安装 Python,如果机器上没有提前安装好相关依赖,很可能编译失败而导致无法安装 Python。

解决思路:

  • Mac 上通过 brew install python-dev 安装依赖即可
  • Linux 相对麻烦。因为依赖项较多,有可能不能一次装完整。以 CentOS 为例,需要安装这些依赖 dnf install bzip2-devel expat-devel gdbm-devel ncurses-devel openssl-devel readline-devel sqlite-devel tk-devel xz-devel zlib-devel wget

第二个问题大家可能会遇到。具体出错过程是这样的:当前项目已经设置了 Python 2.7.18,但尝试执行 python 时提示无法找到命令。

-w859

解决思路

add below 2 lines to ./zshrc IS THE ANSWER, GIVEN THAT you already have eval “$(pyenv init -)”

1
2
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/shims:$PATH"

通常来说,你只要执行 eval "$(pyenv init -)" 就能解决问题。如果还是不行,就试试按上面的指令手动修改 PYENV_ROOTPATH 这两个环境变量。

(换句话说,eval "$(pyenv init -)" 其实不过是在帮你修改 PYENV_ROOTPATH

这里对第二个问题稍作展开。首先,需要理解 pyenv 的工作机制

  • 关键点一, pyenv 在 PATH 环境变量最前面插入了一个 shims 目录。你看到的 PATH 其内容应该是类似 $(pyenv root)/shims:/usr/local/bin:
  • 关键点二, .python-version 文件记录了当前使用的 Python 版本。

假设当前的工作目录是 a_proj_root,这个项目使用 pyenv local 2.7.18 设置过 Python 环境,所以目录中会有一个 .python-version。文件内容是 2.7.18

当你在命令行下输入 python main.py 时,PATH 环境变量中 shims 目录下同名的 python 脚本最先会被执行。脚本内容如下:

-w812

shims 目录下 python 脚本实际路径是 /Users/<user>/.pyenv/shims/python,那么 program 值为 python$@ 值为 main.py。即,实际执行的命令是 pyenv python main.py

pyenv 命令从 a_proj_root 项目的 .python-version 文件中读取到版本号后选择对应版本的 Python 环境来运行 main.py

venv 的使用

venv 的使用相当简单。不过如果你本地有 VS Code,仍然强烈建议你通过 VS Code 内置的入口来创建 Python 虚拟环境,以进一步简化操作过程。

VS Code > 状态栏右侧 > Python 版本号,点击后弹出如下窗口。

-w1156

另外要注意的是实际的 Python 项目通常会安装很多依赖。如果要切换环境,注意确保各个环境均正确地安装地安装了依赖。

Node.js 和 Flutter

nvm(Node Version Manager)是一个用于管理 Node.js 版本的工具,而 fvm (Flutter Version Management) 用于管理和切换不同的 Flutter 版本。

nvm 和 fvm 原理上跟 jenv 及 pyenv 比较类似,均是通过修改环境变量来确保使用指定版本的工具。这里不再展开。

具体的用法,可以参考各自的官方文档。

总结

版本、环境和包/依赖

语言/平台 版本管理 实体环境管理 虚拟环境管理 包管理
Java jenv jenv 或 Gradle/Maven - Gradle/Maven
Python pyenv pyenv venv pip
Node.js nvm nvm - npm
Flutter fvm fvm - flutter pub
  • 版本与环境
    • 版本管理 - 以 Java 为例,可以通过Java版本管理工具(如jenv、jabba 等)或通过手动配置环境变量管理多个不同版本的 Java
    • 环境管理 - 以 Java 为例,通过 jenv 或项目构建工具(如 Maven、Gradle)来设置项目的Java版本来确保不同的Java项目使用不同的 Java 版本,可以通过
    • 包管理 - 以 Java 为例,使用 Maven、Gradle 等包管理工具项目的依赖库和包
  • 从原理上讲,多版本管理均是通过修改环境变量来完成的。

Python 虚拟环境?

为什么 Node.js 和其他编程平台无需虚拟环境,而 Python 需要?

Python 的工作方式导致第三方库和依赖默认安装到全局环境中。相比之下,Java、Node.js 和 Flutter 项目安装依赖时,它们会被安装到项目目录中,而不会影响全局环境。项目目录这种安装方式实现了项目之间的隔离。换句话说,Python(或pip)的依赖管理粒度过粗。

Python 虚拟环境的设计是为了弥补全局安装可能导致的冲突和项目间依赖隔离不足的问题。因此,Python 虚拟环境并非更先进的解决方案,本质上更像是一个补丁。(如果 Python 虚拟环境这个概念让你感到困惑,那并不是你的错,而是 Python 的错)

参考