本文档记录这台 Windows 机器围绕 WSL2 展开的开发环境方案,用于后续迭代和排障。当前决策是:Windows 负责桌面和 GUI,WSL2 负责开发运行时,IDEA 使用 Remote Development -> WSL

先看结论

如果只看这份文档的最终建议,先记住这几条:

  • Windows 只负责 GUI 软件;开发命令、构建、测试、AI agent 都放在 WSL 内运行。
  • 项目源码统一放在 ~/workspace/<project>,不要长期放在 /mnt/c/.../mnt/d/...
  • 代理优先复用 Windows 上的 Clash,通过环境变量让 WSL 内工具走代理。
  • IDEA 使用 Remote Development -> WSL,让 IDE backend、终端、索引和运行配置都在 WSL 内。
  • Node、Java、Python 分别用 nvmSDKMANuv 管理,不混用 Windows 工具链。

推荐执行顺序

第一次搭环境时,按下面顺序往下做更顺:

  1. 在 Windows 侧安装并确认 WSL2。
  2. 在 WSL 内先处理 interop、PATH 边界和代理。
  3. 安装基础工具,再配置 Git / SSH。
  4. 确定项目目录位置,统一迁移到 ~/workspace
  5. 安装 Node、Java、Python 版本管理工具,再按项目需要激活版本。
  6. 最后再接 Docker、Codex、IDEA Remote Development。

目标架构

1
2
3
4
5
6
7
8
9
10
11
12
13
Windows 11
├── IntelliJ IDEA / JetBrains Gateway / JetBrains Client
├── 浏览器、数据库 GUI、抓包工具、设计工具
└── Docker Desktop(可选,开启 WSL integration)

WSL2 Ubuntu
├── ~/workspace/<project> # 代码仓库主目录
├── git / ssh / gpg
├── node / npm / pnpm / yarn
├── java / maven / gradle
├── python / uv / pipx
├── docker cli / docker compose
└── codex / claude / 其他 CLI agent

核心原则:

  • 项目源码长期放在 WSL 文件系统:~/workspace/<project>
  • 不把主力项目放在 /mnt/c/.../mnt/d/...,避免 I/O、权限、符号链接和文件监听问题。
  • 每个语言生态的 SDK、包管理器、依赖缓存都安装在 WSL 内。
  • Windows 侧只保留 GUI 软件;命令行开发、构建、测试、AI agent 都在 WSL 内运行。
  • IDEA 走 Remote Development -> WSL,让 IDE backend、终端、索引、运行配置都在 WSL 内。

官方参考(按需查看)

Windows 侧一次性配置

这一节主要是一次性初始化。机器重装、WSL 发行版重建,或要调整 WSL 资源上限时,再回来看。

以管理员身份打开 PowerShell:

1
2
3
4
wsl --install -d Ubuntu
wsl --set-default-version 2
wsl --update
wsl -l -v

如果已经安装过 WSL,重点确认目标发行版是 VERSION 2

1
wsl -l -v

可选:配置 WSL2 资源上限。编辑 %UserProfile%\.wslconfig

1
2
3
4
5
[wsl2]
memory=16GB
processors=8
swap=8GB
localhostForwarding=true

配置后重启 WSL:

1
wsl --shutdown

资源值按机器实际内存和 CPU 调整。内存紧张时先降低 memory,大型 Java / Node 项目再逐步上调。

WSL 基础环境

这一节完成 WSL 的基础可用状态,顺序是:进入 Ubuntu -> 定义 WSL 与 Windows 的边界 -> 配好网络和代理 -> 安装基础工具 -> 配 Git / SSH。后续 Node、Java、Python、Codex 都依赖这一步。

进入 Ubuntu

1
wsl

Windows interop 与 PATH 边界

WSL 有两个容易混淆的开关:

1
2
3
[interop]
enabled=true
appendWindowsPath=false

推荐写入 WSL 内的 /etc/wsl.conf

1
2
3
4
5
6
7
8
9
10
[boot]
systemd=true

[interop]
enabled=true
appendWindowsPath=false

[automount]
enabled=true
root=/mnt/

修改后在 Windows PowerShell 重启 WSL:

1
wsl --shutdown

含义:

  • enabled=true:允许 WSL 调用 Windows .exe 程序。
  • appendWindowsPath=false:不要把 Windows 的 PATH 自动追加进 WSL 的 PATH
  • automount.enabled=true:继续把 Windows 磁盘挂载到 /mnt/c/mnt/d

这两个 interop 配置不是一回事:

1
2
3
4
5
enabled=true
控制 WSL 能不能执行 Windows .exe

appendWindowsPath=true/false
控制 Windows PATH 要不要追加进 WSL PATH

即使关闭 Windows PATH 注入,只要 enabled=true,仍然可以用完整路径调用 Windows 程序:

1
2
/mnt/c/Windows/explorer.exe .
/mnt/c/Windows/System32/notepad.exe file.txt

如果需要少量短命令,可以自己定义 alias,而不是注入整个 Windows PATH:

1
2
alias explorer='/mnt/c/Windows/explorer.exe'
alias notepad='/mnt/c/Windows/System32/notepad.exe'

验证 WSL 的 PATH 中没有混入 Windows 工具链:

1
echo "$PATH" | tr ':' '\n'

期望不要再看到这些 Windows 路径:

1
2
3
4
/mnt/d/Development/JDK/...
/mnt/d/Development/Node/...
/mnt/d/Development/Maven/...
/mnt/d/Programs/Git/...

这样可以避免 WSL 误用 Windows 的 git.exenode.exejava.exemvn.cmd,保持开发栈由 WSL 内工具链负责。

网络与代理基线

先把代理打通,再继续安装基础工具和语言工具链。后续步骤会访问 Ubuntu 源、GitHub、npm、SDKMAN、PyPI、OpenAI 等站点,代理如果放到最后配,会让中间步骤反复失败。

当前推荐:继续使用 Windows 上的 Clash Verge / Clash Verge Rev,WSL 通过代理端口访问它。默认不在 WSL 里单独安装一套 Clash,除非需要 WSL 在 Windows 代理未启动时独立工作,或需要 Linux 内独立 TUN / 路由规则。

如果 apt update / apt install 因网络失败,可以先切换 Ubuntu 镜像源,或临时按本节的 NAT 方案设置代理后再执行基础安装。

网络模型

WSL2 默认 NAT 模式可以近似理解为:

1
2
3
4
5
6
7
8
9
10
Internet
^
Windows 主机
├── 像 NAT 网关 / 路由器
├── 同时也是普通 Windows 电脑
└── 运行 Clash Verge
└── mixed port,例如 7897

WSL2 Ubuntu
└── 像接在 Windows NAT 后面的一台 Linux 电脑

关键结论:

  • Windows 系统代理不等于 WSL 自动代理。
  • WSL 内的 curlgitnpmpnpmpipcodex 更稳定的方式是显式使用 http_proxy / https_proxy
  • 默认 NAT 下,WSL 通常要通过 Windows host IP 访问 Clash。
  • 启用 mirrored networking 后,WSL 更可能直接通过 127.0.0.1 访问 Windows 上的 Clash 端口。

先选哪种代理方案

推荐顺序很简单:

  • 能用 mirrored networking,就先用 mirrored:配置更直接,WSL 通常可以直接走 127.0.0.1:<port>
  • mirrored 与 VPN、公司网络、防火墙冲突时,再回退到默认 NAT:这时通过 Windows host IP 访问 Clash,并配合 Allow LAN

方案 A:mirrored networking

优先尝试这个方案。根据 Microsoft WSL 文档,mirrored networking 主要面向 Windows 11 22H2 及以上版本。它让 WSL 可以更直接地通过 127.0.0.1 访问 Windows 主机上的服务,也改善 VPN、IPv6、多播等网络兼容性。

编辑 Windows 文件 %UserProfile%\.wslconfig,把网络配置合并到已有 [wsl2] 段:

1
2
3
4
5
6
7
8
[wsl2]
memory=16GB
processors=8
swap=8GB
localhostForwarding=true
networkingMode=mirrored
dnsTunneling=true
autoProxy=true

说明:

  • networkingMode=mirrored:启用 mirrored 网络模式。
  • dnsTunneling=true:让 WSL DNS 查询走 WSL 提供的 tunneling 机制,通常能改善 VPN 和复杂网络下的 DNS 行为。
  • autoProxy=true:尝试把 Windows 代理设置同步给 WSL;即使启用,也建议保留本文的显式 http_proxy / https_proxy 验证流程。

重启 WSL:

1
wsl --shutdown

Windows Clash Verge 保持 mixed port 开启,例如 7897。在 WSL 里测试:

1
curl -I -x http://127.0.0.1:7897 https://github.com

如果成功,WSL 代理地址使用:

1
2
3
export http_proxy=http://127.0.0.1:7897
export https_proxy=http://127.0.0.1:7897
export all_proxy=socks5://127.0.0.1:7897

注意:如果 Clash 的 SOCKS 端口和 HTTP mixed port 分离,all_proxy 要改成实际 SOCKS 端口;如果使用 mixed port,通常 HTTP 和 SOCKS 都能走同一端口。

方案 B:默认 NAT + Allow LAN

如果 mirrored networking 与 VPN、TUN、防火墙或公司网络冲突,回退到 NAT 方案:

  1. Windows Clash Verge 开启 Allow LAN / 局域网连接
  2. 记录 mixed port,例如 7897
  3. WSL 内获取 Windows host IP:
1
2
3
WIN_HOST=$(ip route show | awk '/default/ {print $3; exit}')
echo "$WIN_HOST"
curl -I -x "http://$WIN_HOST:7897" https://github.com

如果成功,WSL 代理地址使用:

1
2
3
4
export WIN_HOST=$(ip route show | awk '/default/ {print $3; exit}')
export http_proxy="http://$WIN_HOST:7897"
export https_proxy="http://$WIN_HOST:7897"
export all_proxy="socks5://$WIN_HOST:7897"

安全注意:Allow LAN 可能把代理端口暴露给局域网。公共 Wi-Fi、公司网络、校园网中要谨慎,必要时用 Windows 防火墙限制只允许 WSL 虚拟网段访问。

推荐 shell 配置

先手动验证代理地址可用,再把下面的函数写入 ~/.bashrc~/.zshrc

这里要注意一件事:mirrored networking默认 NAT 只选一种,不要把两套同名的 proxy_on / proxy_off 一起复制进去。

mirrored networking:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 开启代理:直接走 Windows localhost mixed port
proxy_on() {
export http_proxy=http://127.0.0.1:7897
export https_proxy=http://127.0.0.1:7897
export all_proxy=socks5://127.0.0.1:7897
export no_proxy=localhost,127.0.0.1,::1

# 大写变量:兼容部分只认大写环境变量的工具
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export ALL_PROXY="$all_proxy"
export NO_PROXY="$no_proxy"
}

# 关闭代理:清空大小写两套环境变量
proxy_off() {
unset http_proxy https_proxy all_proxy no_proxy
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY
}

默认 NAT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 开启代理:自动取 WSL 默认网关作为 Windows host IP
proxy_on() {
local win_host
win_host=$(ip route show | awk '/default/ {print $3; exit}')

# 代理变量:给 curl、git、npm、pip、codex 等 CLI 使用
export http_proxy="http://$win_host:7897"
export https_proxy="http://$win_host:7897"
export all_proxy="socks5://$win_host:7897"
export no_proxy=localhost,127.0.0.1,::1

# 大写变量:兼容部分只认大写环境变量的工具
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export ALL_PROXY="$all_proxy"
export NO_PROXY="$no_proxy"
}

# 关闭代理:清空大小写两套环境变量
proxy_off() {
unset http_proxy https_proxy all_proxy no_proxy
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY
}

可选:如果希望命令行一眼看出当前是否处于代理状态,再补上这一段。它同时适用于 mirrored 和 NAT。

不要直接把 PS1 改成一整段纯文本,否则原来提示符里的颜色、Git 分支、虚拟环境信息都可能被覆盖。更稳妥的方式是:只保存一次原始 PROMPT_COMMAND 和基础提示符;每次刷新时先恢复基础提示符,再执行原来的提示符逻辑,最后在前面加一个状态标记。这样不会把 [PROXY] / [DIRECT] 一层层叠上去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 只保存一次原始 PROMPT_COMMAND,避免重复 source 后把自己套进去
if [[ -z "${__ORIGINAL_PROMPT_COMMAND_SAVED:-}" ]]; then
__ORIGINAL_PROMPT_COMMAND="${PROMPT_COMMAND:-}"
__ORIGINAL_PROMPT_COMMAND_SAVED=1
fi

# 只保存一次基础提示符
if [[ -z "${__BASE_PS1_ORIG:-}" ]]; then
__BASE_PS1_ORIG="$PS1"
fi

# 根据代理状态生成前缀
set_proxy_prompt() {
local proxy_tag

if [[ -n "${http_proxy:-}${HTTP_PROXY:-}" ]]; then
# 柔和琥珀色
proxy_tag='\[\e[38;5;179m\][PROXY]\[\e[0m\]'
else
# 柔和青绿色
proxy_tag='\[\e[38;5;109m\][DIRECT]\[\e[0m\]'
fi

PS1="${proxy_tag} ${PS1}"
}

# 先恢复基础提示符,再执行原来的提示符逻辑,最后加代理状态
__update_prompt() {
PS1="$__BASE_PS1_ORIG"

if [[ -n "${__ORIGINAL_PROMPT_COMMAND:-}" && "${__ORIGINAL_PROMPT_COMMAND}" != "__update_prompt" ]]; then
eval "$__ORIGINAL_PROMPT_COMMAND"
fi

set_proxy_prompt
}

PROMPT_COMMAND="__update_prompt"

整体示例按 Bash 编写,适合放进 ~/.bashrc;如果你使用 Zsh,思路相同,只是要改成 PROMPT / precmd 写法。提示符效果如下:

  • 开启代理后显示柔和琥珀色的 "[PROXY]"
  • 关闭代理后显示柔和青绿色的 "[DIRECT]"

使用方式:

如果当前 shell 已经出现 [DIRECT] [DIRECT] 这种重复前缀,优先直接开一个新 shell:

1
exec bash

平时修改配置后,普通刷新可直接执行:

1
2
3
4
source ~/.bashrc
proxy_on
proxy_off
env | grep -i proxy

安装基础工具

如果代理已经通过环境变量配置好,apt 使用 sudo -E 保留代理变量:

1
2
3
sudo -E apt update
sudo -E apt upgrade -y
sudo -E apt install -y git curl ca-certificates build-essential ripgrep fd-find jq unzip zip rsync iproute2 tree btop tmux

如果当前网络不需要代理,也可以直接使用普通 sudo apt ...

如果执行 sudo -E apt ... 时看到下面提示,说明当前 sudoers 不允许完整保留环境变量:

1
sudo: preserving the entire environment is not supported, '-E' is ignored

这时不要依赖 sudo -E 传递代理,改用 apt 自己的代理参数:

1
2
3
4
5
6
7
8
9
sudo apt \
-o Acquire::http::Proxy="$http_proxy" \
-o Acquire::https::Proxy="$https_proxy" \
update

sudo apt \
-o Acquire::http::Proxy="$http_proxy" \
-o Acquire::https::Proxy="$https_proxy" \
install -y git curl ca-certificates build-essential ripgrep fd-find jq unzip zip rsync iproute2 tree btop tmux

后续所有 sudo -E apt ... 命令遇到同样提示时,都按这个方式把代理显式传给 apt

常用辅助工具:

1
2
3
4
5
6
7
8
tree
查看目录树,适合快速确认项目结构

btop
查看 CPU、内存、进程和磁盘状态

tmux
复用终端会话,适合长时间运行命令或保留多窗口工作区

验证:

1
2
3
command -v tree
command -v btop
command -v tmux

可选:让 Ubuntu 的 fd-find 使用常见命令名 fd

1
2
mkdir -p ~/.local/bin
ln -s "$(command -v fdfind)" ~/.local/bin/fd

mkdir -p ~/.local/bin 只负责创建目录,不会保证该目录已经在 PATH 中。先检查:

1
echo "$PATH" | tr ':' '\n' | grep "$HOME/.local/bin"

如果没有输出,写入 shell 配置:

1
2
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

验证:

1
command -v fd

Locale 与中文文件名显示

如果在 WSL 中创建中文文件名后,ls 显示成下面这种形式:

1
''$'\345\223\210\345\223\210\345\223\210''.text'

或执行 ls -N 后显示成问号:

1
????????.text

优先检查 locale:

1
2
locale
echo "$LANG"

如果当前是 C.UTF-8,但中文文件名仍显示异常,建议切到明确的 en_US.UTF-8。这样命令输出仍以英文为主,同时可以正常显示中文文件名。

1
2
3
4
5
6
7
sudo -E apt update
sudo -E apt install -y locales

sudo locale-gen en_US.UTF-8
sudo update-locale LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8

exec bash -l

exec bash -l 只是替换当前 shell,很多时候够用;如果执行后 ls -N 仍显示问号,直接关闭当前 WSL 终端窗口,再重新打开一个新终端。update-locale 写入的是后续登录会话的默认环境,新终端会更完整地重新加载。

验证:

1
2
3
locale
touch 哈哈哈.text
ls -N

期望能直接看到:

1
哈哈哈.text

如果只是 Git 输出里的中文路径被转义,例如 \345\223\210,那是 Git 的显示配置问题,按下一节设置 core.quotePath=falselocale 影响 ls、shell、终端里的文件名显示;core.quotePath=false 只影响 Git 输出。

Git 与 SSH

配置 Git 身份:

1
2
3
4
5
git config --global user.name "你的名字"
git config --global user.email "你的邮箱"
git config --global init.defaultBranch main
git config --global core.autocrlf input
git config --global core.quotePath false

core.quotePath=false 用来让 Git 直接显示中文文件名。否则 git status / git diff --name-only 可能把中文路径转成八进制转义,例如:

1
"learn/language/english/\345\210\235\344\270\255\346\261\211\350\257\221\350\213\261200\351\242\230.md"

设置后会正常显示:

1
learn/language/english/初中汉译英200题.md

生成 SSH key:

1
2
ssh-keygen -t ed25519 -C "你的邮箱"
cat ~/.ssh/id_ed25519.pub

把公钥添加到 GitHub / GitLab / Gitee 等代码托管平台。

工具级代理补充

大多数 CLI 会读取环境变量。只有在工具不生效时,再写工具级配置。

Git 可选配置:

1
2
git config --global http.proxy "$http_proxy"
git config --global https.proxy "$https_proxy"

取消 Git 代理:

1
2
git config --global --unset http.proxy
git config --global --unset https.proxy

npm 可选配置:

1
2
npm config set proxy "$http_proxy"
npm config set https-proxy "$https_proxy"

pnpm 通常读取 npm 配置和环境变量,优先确认:

1
2
pnpm config get proxy
pnpm config get https-proxy

pip 一般读取环境变量;临时指定:

1
pip install --proxy "$https_proxy" <package>

验证命令

1
2
3
4
env | grep -i proxy
curl -I https://github.com
curl -I https://api.openai.com
git ls-remote https://github.com/openai/codex.git HEAD

如果 curl -I https://github.com 不走代理或失败,先用 curl -I -x <proxy-url> https://github.com 验证代理端口,再检查 shell 环境变量。Node.js 安装完成后,再用 npm view @openai/codex version 验证 npm 访问。

何时在 WSL 内单独安装 Clash

默认不建议。只有满足以下条件时再考虑:

  • Windows 不开 Clash 时,WSL 仍要独立代理。
  • WSL 要作为长期 headless 开发环境使用。
  • 需要 Linux 内独立 TUN、DNS、透明代理或路由规则。
  • Windows Clash 与 WSL、VPN、公司网络长期冲突。

代价:

  • Clash 配置、订阅、规则要维护两套。
  • Windows 与 WSL 可能出现双重代理、DNS 分流不一致。
  • TUN / 防火墙 / 端口监听排障复杂度更高。

项目目录与工具链

代码位置确定后,再处理语言工具链。这里的目标不是一次把所有工具都装全,而是先建立统一的目录约定和版本管理方式,让不同项目能各自锁定版本。

目录约定

这一节只解决一个问题:项目放哪

1
mkdir -p ~/workspace

目前只定义了代码仓库根目录:

1
~/workspace/<project>

现有 Windows 项目迁移时,优先重新 clone:

1
2
3
cd ~/workspace
git clone <repo-url> docs
cd docs

如果项目暂时没有远程仓库,可以从 Windows 盘复制一次:

1
2
mkdir -p ~/workspace/docs
rsync -a --exclude node_modules --exclude .idea /mnt/d/Workspaces/docs/ ~/workspace/docs/

复制只是迁移手段,后续开发仍以 ~/workspace/docs 为主。

工具版本管理原则

各语言平台使用各自生态内成熟的版本管理工具:

1
2
3
4
5
Node.js    -> nvm + Corepack
Java -> SDKMAN
Python -> uv
Maven -> 项目自带 mvnw 优先,缺失时 SDKMAN
Gradle -> 项目自带 gradlew 优先,缺失时 SDKMAN

不把 update-alternatives 当作项目级版本管理方案。它适合系统级默认命令选择,例如系统的 /usr/bin/java 指向哪个 JDK;不适合“项目 A 用 Java 17,项目 B 用 Java 21”这种按项目切换的开发场景。

总体分工:

  • 语言运行时版本由语言生态自己的工具管理。
  • 项目内版本约束尽量写入项目文件,例如 .nvmrc.sdkmanrcpackageManager.python-version
  • 构建工具优先使用项目 wrapper,例如 mvnwgradlew
  • WSL 内不要混用 Windows 的 Node、JDK、Maven、Gradle、Python。

下面按 Node -> Java -> Python 的顺序安装。Maven / Gradle 跟在 Java 后面处理。

Node.js:nvm + Corepack

Node 生态分两层:

1
2
nvm       管 Node.js 版本
Corepack 管 pnpm / yarn 版本入口

再细一点,可以把 nvmnpmCorepackpnpm 分成四层:

1
2
3
4
nvm      管 Node 版本
npm 跟着 Node 自带,是默认包管理器
Corepack 管 pnpm / yarn 这类包管理器的版本入口
pnpm 真正负责安装项目依赖

推荐边界:

  • nvm 只负责 Node 版本。
  • npm 保留,不卸载;主要用于少量全局 CLI,例如 Codex。
  • Corepack 负责启用和固定 pnpm / yarn 版本。
  • pnpm 负责项目依赖,例如 pnpm installpnpm addpnpm run dev
  • 不要让 npmpnpm 同时管理同一个全局命令。

需要特别注意:nvm 下的 npm 全局包是跟 Node 版本绑定的。比如当前 Node 在:

1
~/.nvm/versions/node/v22.22.3/bin/node

那么 npm install -g <package> 安装出的全局命令也会在同一个 Node 版本目录下。以后切到另一个 Node 大版本时,全局命令会变成另一套。

安装 nvm

1
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash

关闭并重新打开 WSL 终端后:

1
2
3
4
nvm install 22
nvm alias default 22
node -v
npm -v

项目可用 .nvmrc 固定 Node 版本:

1
22

nvm 没有单独的 .nvmrc 初始化子命令。通常直接在项目根目录写入版本,例如:

1
2
cd ~/workspace/<project>
echo "22" > .nvmrc

如果想按当前已激活的 Node 版本初始化,也可以用:

1
node -v | sed 's/^v//' > .nvmrc

进入项目后:

1
2
nvm install
nvm use

命令频率:

1
2
3
4
5
6
7
8
nvm install
第一次进入项目、新机器、新 WSL、项目升级 Node 版本时执行。

nvm use
每个新 shell / 新终端会话进入项目后执行一次。

nvm alias default 22
设置默认 Node。项目没有 .nvmrc 时,新 shell 默认使用该版本。

启用 Corepack,用于 pnpm / yarn。Corepack 不管理 Node 版本,它在当前 Node 版本下管理 pnpm / yarn 入口,并可读取 package.json 中的 packageManager 字段:

1
2
corepack enable pnpm
pnpm -v

corepack enable 通常在每个 Node 大版本安装后执行一次,不需要每次进入项目都执行。它会启用 pnpm / yarn 的 shim;执行 pnpmyarn 时,shim 会读取当前项目的 packageManager,再选择或下载对应版本的包管理器。

示例:

1
2
3
{
"packageManager": "pnpm@11.1.2"
}

如果项目没有 packageManager 字段,建议用 Corepack 补上,而不是手动全局安装 pnpm:

1
corepack use pnpm@latest-11

这条命令会把当前项目的 packageManager 写入 package.json,并让项目内使用固定的 pnpm 版本。版本号以命令实际写入为准,不需要长期手写 latest

项目内依赖安装只在 WSL 内执行:

1
2
cd ~/workspace/<project>
pnpm install

不要用 Windows Node 去操作 WSL 项目里的 node_modules

总结成一句话:

1
nvm 管 Node,Corepack 管 pnpm,pnpm 管项目,npm 管少量全局 CLI。

Java:SDKMAN

SDKMAN 是 JVM 生态的用户级版本管理器,可管理 Java / Maven / Gradle / Kotlin / Scala 等工具。

在这套 WSL 开发栈里,推荐把它当作 JVM 工具链的项目级版本入口:

  • Java 版本由 SDKMAN 管
  • Maven / Gradle 在没有项目 wrapper 时,再交给 SDKMAN 管
  • 项目内版本约束尽量写进 .sdkmanrc

先安装 SDKMAN:

1
curl -s "https://get.sdkman.io" | bash

安装完成后,重新打开终端;如果想在当前 shell 里立即生效,也可以执行:

1
2
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk version

然后安装 JDK:

1
2
3
sdk list java
sdk install java <从列表中选择的 JDK 21 版本>
java -version

sdk list java 会列出不同 JDK 发行版。默认开发可优先选 Temurin,也就是版本后缀通常为 -tem 的条目。安装时 SDKMAN 会询问是否设为默认版本,也可以手动设置:

1
sdk default java <已安装的 Java 版本>

接着把项目需要的版本沉淀到 .sdkmanrc。不必手动创建,直接在项目根目录执行:

1
2
cd ~/workspace/<project>
sdk env init

sdk env init 会按当前 shell 已激活的版本生成 .sdkmanrc。如果当前只激活了 Java,初始内容通常只有 java=...;后续可以按项目要求补上 mavengradle。例如:

1
2
3
java=<从 sdk list java 中选择的版本>
maven=<项目要求的 Maven 版本>
gradle=<项目要求的 Gradle 版本>

进入项目后,先补齐缺失版本,再切换到项目要求的版本:

1
2
3
cd ~/workspace/<project>
sdk env install
sdk env

含义:

1
2
3
4
5
sdk env install
按 .sdkmanrc 安装缺失版本。

sdk env
按 .sdkmanrc 切换当前 shell 的 Java / Maven / Gradle 等版本。

如果只是临时切换当前 shell,可用:

1
sdk use java <已安装的 Java 版本>

IDEA 里如果需要手动指定 JDK 路径,可用:

1
sdk home java <已安装的 Java 版本>

Maven / Gradle

Maven / Gradle 的处理顺序建议固定为:

1
2
3
优先使用项目 wrapper
-> 没有 wrapper 时,再用 SDKMAN 安装对应版本
-> 需要长期固定时,写入 .sdkmanrc

只要项目自带 wrapper,就优先用 wrapper:

1
2
./mvnw -v
./gradlew -v

wrapper 会固定 Maven / Gradle 版本,和项目 CI、插件兼容性最一致。Gradle 对版本更敏感,Android Gradle Plugin、Kotlin Gradle Plugin、Java Toolchain 等都可能要求特定组合。

没有 wrapper 时,不要直接安装最新版本。先看 README、CI、pom.xmlbuild.gradlesettings.gradle,确认项目要求,再用 SDKMAN 安装:

1
2
3
4
sdk install maven <项目要求的版本>
sdk install gradle <项目要求的版本>
mvn -v
gradle -v

如果这个项目会长期维护,建议把版本补回 .sdkmanrc,避免换机器或换 shell 后靠记忆恢复:

1
2
3
java=<项目要求的 Java 版本>
maven=<项目要求的 Maven 版本>
gradle=<项目要求的 Gradle 版本>

Python 与 uv

Python 使用 uv 管理解释器、虚拟环境和依赖。它和 Node / Java 不太一样,uv 同时覆盖了传统 Python 工具链里的多层能力:

1
2
3
4
5
uv python install  管 Python 解释器
uv venv 管虚拟环境
uv pip 兼容 pip 风格依赖安装
uv sync 按 pyproject.toml / uv.lock 同步依赖
uv tool 类似 pipx,安装 Python CLI 工具
1
2
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version

传统 requirements.txt 项目:

1
2
3
4
5
cd ~/workspace/<project>
uv python install 3.12
uv venv
source .venv/bin/activate
uv pip install -r requirements.txt

现代 pyproject.toml 项目:

1
2
3
cd ~/workspace/<project>
uv python install 3.12
uv sync

如果要固定 Python 版本,可以在项目根目录放 .python-version

1
3.12

然后执行:

1
2
uv python install
uv sync

版本生效检查

IDEA Remote Development 运行在 WSL 后端时,项目 SDK / Node interpreter / Python interpreter 应指向 WSL 内工具链,而不是 Windows 工具链。

查看实际路径:

1
2
3
4
5
6
which node
node -v
which java
java -version
which python
python --version

Codex 推荐从 WSL 项目目录启动:

1
2
cd ~/workspace/<project>
codex

这样 Codex 继承当前 shell 的 PATH,后续运行 nodejavapythonnpm test./mvnw test 等命令时会使用 WSL 内当前激活的版本。

如果 Codex、IDEA 或非交互脚本里发现版本不对,优先检查:

1
2
3
4
5
6
which node
node -v
which java
java -version
which python
python --version

开发工具集成

基础工具和语言版本管理完成后,再处理 Docker、Codex 和 IDEA 这类开发工具。

Docker

优先方案:Docker Desktop 做唯一 daemon,WSL Ubuntu 只作为客户端环境

先分清两条路线:

1
2
3
4
5
6
7
8
9
10
11
路线 A:Docker Desktop
Windows 安装 Docker Desktop
-> Docker Desktop 创建并管理 docker-desktop WSL2 发行版
-> Docker daemon 运行在 Docker Desktop 管理的后端环境里
-> Docker Desktop 通过 WSL integration 把访问入口暴露给 Ubuntu
-> Ubuntu 里的 docker CLI 通过 /var/run/docker.sock 访问这个 daemon

路线 B:WSL 原生 Docker
在 Ubuntu 里直接 apt install docker-ce / containerd.io
-> Ubuntu 自己运行 dockerd / containerd
-> Ubuntu 里的 docker CLI 访问 Ubuntu 自己的 daemon

路线 A 不是“把 Docker Engine 安装进默认 Ubuntu”。默认 Ubuntu 里执行的是客户端命令,真正的 daemon 在 Docker Desktop 管理的 docker-desktop 发行版中。访问链路是:

1
2
3
4
Ubuntu docker CLI
-> /var/run/docker.sock
-> Docker Desktop WSL integration
-> docker-desktop 发行版中的 Docker daemon

这两条路线都能用,但不要同时维护两套 daemon。混用时容易出现:

  • Docker Desktop 已经启用 WSL integration。
  • WSL 里原生 docker.service / containerd 也在运行。
  • docker context/var/run/docker.sockdesktop-linux context 互相干扰。
  • docker version 报 socket 不存在、权限错误,或连接到不是预期的 daemon。

目标状态:

1
2
3
4
5
6
7
8
9
10
11
WSL Ubuntu
保留 docker CLI / compose / buildx
不运行 dockerd / containerd

Docker Desktop
负责运行 daemon
负责镜像、容器、卷等运行时状态

docker version 在 WSL 中显示
Client: Docker Engine ...
Server: Docker Desktop

Docker Desktop 设置项:

  • Docker Desktop -> Settings -> General -> Use the WSL 2 based engine。
  • Docker Desktop -> Settings -> Resources -> WSL Integration -> 启用目标 Ubuntu。

如果 WSL 里原来装过原生 Docker Engine,先停掉并卸载 daemon 侧组件:

1
2
3
sudo systemctl disable --now docker docker.socket containerd
sudo apt purge -y docker-ce containerd.io
sudo apt autoremove -y

客户端组件可以保留:

1
2
3
docker-ce-cli
docker-buildx-plugin
docker-compose-plugin

然后在 Windows PowerShell 重启 WSL:

1
wsl --shutdown

重新进入 Ubuntu 后验证:

1
2
3
4
5
which docker
docker context ls
docker version
docker compose version
docker ps

期望:

1
2
3
4
5
6
7
8
docker.service / docker.socket / containerd
在 WSL Ubuntu 内不运行

/var/run/docker.sock
由 Docker Desktop WSL integration 暴露

docker version
能看到 Server: Docker Desktop

可以用下面命令确认 WSL 原生 daemon 没有运行:

1
2
3
pgrep -a dockerd
pgrep -a containerd
systemctl is-active docker docker.socket containerd

如果项目强依赖 Linux 原生 Docker Engine,再单独评估是否在 WSL 内安装 Docker Engine。默认不要同时维护 Docker Desktop 和 WSL 原生 Docker 两套 daemon。

注意:Windows 侧的 desktop-linux context 通常指向:

1
npipe:////./pipe/dockerDesktopLinuxEngine

这是 Windows Docker CLI 使用的 endpoint。WSL 里通常继续使用 default context,并通过 Unix socket 访问 Docker Desktop 暴露进来的 daemon:

1
default -> unix:///var/run/docker.sock

Docker Desktop 的代理也和 WSL shell 代理不同。Docker Desktop 是 Windows 应用,它可以读取 Windows 系统代理或 Docker Desktop 自己的代理设置,再把配置传给后端 daemon;所以即使 Ubuntu shell 没有配置 http_proxydocker pull 也可能正常走 Windows 侧代理。

三层代理边界:

1
2
3
4
5
6
7
8
Docker Desktop daemon
主要看 Windows 系统代理 / Docker Desktop 代理设置

WSL Ubuntu shell
主要看 http_proxy / https_proxy 环境变量

WSL 原生 dockerd
主要看 systemd drop-in / daemon 环境配置

因此,docker pull 能走代理,不代表 Ubuntu 里的 curlgitnpmpnpmcodex 都会自动走代理。Shell 工具仍按本文“网络与代理基线”一节配置。

如果在 Codex 沙箱内执行 docker version 出现 permission denied,而普通 WSL shell 中正常,优先按沙箱隔离看待,不要直接判断 Docker Desktop 集成失败。

Codex CLI

Codex 安装在 WSL 内。先确认 Node 来自 WSL 内的 nvm:

1
2
node -v
npm -v

安装:

1
2
npm i -g @openai/codex
codex

升级:

1
npm i -g @openai/codex@latest

使用方式:

1
2
cd ~/workspace/<project>
codex

Codex 全局安装边界

在这套 WSL 环境里,Codex 建议只保留 npm 全局安装这一份:

1
2
Codex CLI -> npm global
pnpm -> 项目依赖,不负责 Codex 这个全局命令

原因是当前 Node 由 nvm 管理,npm 全局命令会跟随当前 Node 版本目录,路径更直观:

1
~/.nvm/versions/node/<version>/bin/codex

不要同时用 npm 和 pnpm 安装同一个全局命令。例如同时存在下面两份,就属于混装:

1
2
~/.local/share/pnpm/bin/codex
~/.nvm/versions/node/<version>/bin/codex

整理成 npm 单一来源:

1
2
3
4
5
pnpm remove -g @openai/codex
npm install -g @openai/codex@latest
hash -r
which -a codex
codex --version

最终期望 which -a codex 只看到 npm 这一份:

1
~/.nvm/versions/node/<version>/bin/codex

如果执行 pnpm remove -g @openai/codex 后,再运行 codex resume 出现类似错误:

1
bash: /home/<user>/.local/share/pnpm/bin/codex: No such file or directory

这通常是 bash 命令路径缓存导致的。文件已经被删掉,但当前 shell 还记着旧路径。处理方式:

1
2
3
4
hash -r
which -a codex
codex --version
codex resume

如果 which -a codex 没有任何输出,说明 npm 这一份也不存在,重新安装即可:

1
2
3
4
npm install -g @openai/codex@latest
hash -r
which -a codex
codex --version

Codex sandbox 前置条件

根据 OpenAI 官方文档,Codex 在 Linux 上默认使用 bwrap + seccomp 实现 sandbox;在 Windows 的 WSL 场景下使用 WSL2 内的 Linux sandbox。从 Codex 0.115 开始,WSL1 不再支持 Linux sandbox,主力环境应使用 WSL2。

WSL2 / Linux 上应优先使用系统包提供的 bubblewrap,不要依赖 bundled helper 的回退路径。

WSL2 Ubuntu / Debian 基础安装:

1
2
sudo -E apt update
sudo -E apt install -y bubblewrap

Ubuntu 24.04 额外补齐 AppArmor profile:

1
2
3
4
5
sudo -E apt install -y apparmor-profiles apparmor-utils
sudo install -m 0644 \
/usr/share/apparmor/extra-profiles/bwrap-userns-restrict \
/etc/apparmor.d/bwrap-userns-restrict
sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict

注意事项:

  • 需要代理时优先用 sudo -E apt ...;如果 sudo -E 被 sudoers 忽略,按“安装基础工具”一节的方式用 apt -o Acquire::http::Proxy=... 显式传代理。
  • 优先补齐 bubblewrap 和 Ubuntu 24.04 的 AppArmor profile,不要直接用关闭内核限制的方式绕过。
  • sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 只作为临时兜底,不建议写成默认配置。
  • 对 Codex 的验收标准以实际启动结果为准:codex --sandbox workspace-write --ask-for-approval on-requestcodex --sandbox read-only 启动无 warning 即可。
  • 也可以用 codex sandbox linux <command> 单独验证 Linux sandbox 行为。
  • WSL 下 AppArmor 的挂载状态可能和原生 Ubuntu 不完全一致;只要 Codex 无 sandbox 相关 warning,就不把这类系统差异当阻塞项。

预期效果:

  • Codex 的 shell 是 Linux/bash。
  • Codex 看到的路径是 /home/<user>/workspace/<project>
  • Codex 继承启动它的 WSL shell 环境,正常情况下会使用当前 shell 激活的 Node、Java、Python 版本。
  • 读写文件、运行测试、调用 Git 都发生在 WSL 内。

IDEA Remote Development -> WSL

目标:IDEA backend 跑在 WSL2,JetBrains Client 跑在 Windows。

入口:

1
2
3
4
5
6
7
8
IntelliJ IDEA Welcome Screen
-> Remote Development
-> WSL
-> New Connection
-> 选择 Ubuntu
-> 选择 IDE 版本
-> 选择 /home/<user>/workspace/<project>
-> Start IDE and Connect

连接后检查:

  • Terminal 路径应为 /home/<user>/workspace/<project>,而不是 C:\...D:\...
  • Git 应来自 WSL,例如 /usr/bin/git
  • Java SDK / Node interpreter / Python interpreter 应来自 WSL 内工具链,可用 which javawhich nodewhich python 查看。
  • Maven / Gradle / npm / pnpm 命令应在 WSL 内执行。
  • 插件分清位置:语言、框架、构建工具相关插件安装到 remote backend;主题、键位、UI 类插件主要影响 JetBrains Client。
  • 代理相关环境变量应在 WSL shell 中可见,例如 echo $https_proxy

如果 Remote Development 体验明显卡顿,先排查:

1
2
3
4
5
pwd
git status
df -h
free -h
top

再在 Windows PowerShell 检查:

1
wsl -l -v

优先确认项目没有放在 /mnt/c/mnt/d 下。

常用日常命令

这一节按日常场景分组,后续排障时可以直接跳到对应小块。

项目与工具

启动项目开发:

1
2
cd ~/workspace/<project>
codex

更新系统:

1
2
sudo apt update
sudo apt upgrade -y

更新 Codex:

1
npm i -g @openai/codex@latest

查看当前项目工具版本:

1
2
3
4
5
6
which node
node -v
which java
java -version
which python
python --version

查看同名命令的所有来源:

1
2
which -a codex
which -a pnpm

清理当前 bash 记住的旧命令路径:

1
hash -r

WSL 状态

查看 WSL 发行版:

1
wsl -l -v

重启 WSL:

1
wsl --shutdown

代理与网络

查看代理环境:

1
env | grep -i proxy

临时为当前 shell 设置 mirrored 代理:

1
2
3
export http_proxy=http://127.0.0.1:7897
export https_proxy=http://127.0.0.1:7897
export all_proxy=socks5://127.0.0.1:7897

临时为当前 shell 设置 NAT 代理:

1
2
3
4
export WIN_HOST=$(ip route show | awk '/default/ {print $3; exit}')
export http_proxy="http://$WIN_HOST:7897"
export https_proxy="http://$WIN_HOST:7897"
export all_proxy="socks5://$WIN_HOST:7897"

Windows 侧访问 WSL 文件

从 Windows Explorer 打开 WSL home:

1
\\wsl.localhost\Ubuntu\home\<user>

性能与稳定性清单

这份清单适合在“感觉哪里不对”但还没定位到具体问题时,先快速过一遍。

  • 主力仓库放在 ~/workspace,不要放在 /mnt/c/mnt/d
  • 依赖目录只由 WSL 工具链写入,例如 node_modules.venvtargetbuild
  • 不混用 Windows Git 和 WSL Git 处理同一个工作区。
  • 不混用 Windows Node 和 WSL Node 处理同一个项目。
  • 不让 npm 和 pnpm 同时管理同一个全局命令;例如 Codex 只保留 npm 或 pnpm 其中一份。
  • Java、Node、Python 等开发工具使用各自生态的版本管理工具;update-alternatives 只处理系统级默认命令,不作为项目版本管理方案。
  • 大型项目优先检查 .wslconfig 的内存和 CPU 限制。
  • Docker Desktop 只开启需要的 WSL distro integration。
  • Remote Development 连接异常时,先 wsl --shutdown,再重新连接。
  • 如果 IDE 运行/调试被防火墙拦截,按 IDEA WSL 文档检查 Windows Firewall 的 WSL 规则。
  • 代理优先使用 Windows Clash + WSL 环境变量,不默认在 WSL 内再装一套 Clash。
  • mirrored 代理优先测试 127.0.0.1:<mixed-port>;NAT 代理优先测试 $(ip route show | awk '/default/ {print $3; exit}'):<mixed-port>
  • 公共网络中使用 Clash Allow LAN 时,要确认 Windows 防火墙没有把代理端口暴露给整个局域网。

迭代记录

  • 2026-06-01:补充 WSL locale 与中文文件名显示排障,区分 ls 终端显示问题和 Git core.quotePath=false 路径显示问题。
  • 2026-05-31:补充 Docker Desktop 与 WSL 原生 Docker 的区别,记录从 WSL 原生 Docker 收敛到 Docker Desktop daemon 的处理流程和验证标准。
  • 2026-05-31:整理 nvmnpmCorepackpnpm 的职责边界;补充 Codex 被 npm / pnpm 混装后的处理流程,以及 bash hash -r 路径缓存排障说明。
  • 2026-05-26:按 Microsoft WSL 网络文档和 OpenAI Codex sandbox 文档复核 mirrored networking、DNS tunneling、autoProxy、WSL2 sandbox 与 WSL1 支持边界;补充 codex sandbox linux 本地验证命令。
  • 2026-05-14:补充“先看结论”和“推荐执行顺序”,重组“项目目录与工具链”“开发工具集成”层级,按阅读路径整理代理、工具链和日常命令章节。
  • 2026-05-04:初始化 WSL2 开发栈文档,明确使用 IDEA Remote Development -> WSL。
  • 2026-05-04:补充 Windows Clash 与 WSL 代理方案,覆盖 mirrored networking、默认 NAT、环境变量和工具级验证。
  • 2026-05-04:补充 WSL interop / Windows PATH 边界说明;Java、Node、Python 使用各自生态版本管理工具。
  • 2026-05-05:按 OpenAI Codex sandbox 官方前置条件补充 bubblewrap 与 Ubuntu 24.04 AppArmor 说明,并记录本机当前核对结果。
  • 2026-05-05:确认 codex --sandbox workspace-write --ask-for-approval on-requestcodex --sandbox read-only 启动无 warning,按 Codex 可用状态验收;WSL 下 AppArmor filesystem 未挂载仅作为系统差异保留。