# Tailscale + SSH 远程开发操作指南

## 文档概述

Mac 电脑通过 Tailscale 跨网络 SSH 连接 Windows 电脑上的 Linux 虚拟机（VMware），实现远程 Android 底层开发。

## 当前环境信息（2026-04-17 已验证）

| 项目 | 信息 |
|------|------|
| Mac 电脑 | macOS, ClashX Pro (VPN), Tailscale |
| Windows 电脑 | VMware Workstation 17, Clash for Windows v0.19.10 |
| Linux 虚拟机 | Ubuntu 20.04 LTS, VMware 桥接模式 |
| 虚拟机用户 | zhangwenqi |
| Tailscale IP | 100.123.82.91 |
| 虚拟机局域网 IP | 192.168.6.60 |
| SSH 密钥 | Mac ~/.ssh/id_ed25519 已部署到虚拟机 |
| Docker | 28.1.1 |
| **Radxa CM5 环境** | Docker 镜像 android-builder:12.x (6.48GB, Ubuntu 20.04 基底)，SDK 已编译验证 |
| **OrangePi CM5 环境** | Docker 镜像 android-builder-orangepi:13.x (待构建, Ubuntu 22.04 基底)，SDK 已合并待解压 |
| 磁盘 | 700GB 虚拟磁盘，剩余空间足够双环境并行 |

---

# 第一部分：网络架构

## 1.1 整体架构

```
Mac (ClashX Pro VPN + Tailscale)
  |
  | Tailscale P2P (100.x.x.x)
  |
Linux 虚拟机 (Tailscale, IP: 100.123.82.91)
  |
  | VMware 桥接 (192.168.6.60)
  |
Windows (Clash for Windows, 端口 7890)
```

## 1.2 各设备职责

- Mac: 开发主机，VPN 翻墙 + SSH 远程开发
- Windows: 宿主机，运行 VMware 虚拟机 + Clash 代理
- Linux 虚拟机: 编译服务器，Docker + Android SDK 编译环境

---

# 第二部分：ClashX Pro 与 Tailscale 共存配置（关键）

Mac 上 ClashX Pro（增强模式/TUN）与 Tailscale 存在两个冲突，必须同时解决。

## 2.1 问题一：增强模式流量劫持

ClashX Pro 增强模式创建 TUN 虚拟网卡拦截所有流量，Tailscale 的 WireGuard UDP 流量也被劫持，导致 Tailscale 连不上。

**解决方法：** 在 ClashX Pro 代理配置文件的 rules 最前面添加直连规则。

配置文件路径: `~/.config/clash/你的订阅配置.yaml`

在 `rules:` 下方、第一条规则之前插入：

```yaml
rules:
 - PROCESS-NAME,tailscaled,DIRECT
 - IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
 - IP-CIDR6,fd7a:115c:a1e0::/48,DIRECT,no-resolve
 - DOMAIN-SUFFIX,tailscale.com,DIRECT
 - DOMAIN-SUFFIX,login.tailscale.com,DIRECT
 # ... 原有规则保持不变 ...
```

各规则作用：
- `PROCESS-NAME,tailscaled,DIRECT` → Tailscale 进程所有流量直连（最关键）
- `IP-CIDR,100.64.0.0/10,DIRECT` → Tailscale 内网 IP 段直连
- `IP-CIDR6,fd7a:115c:a1e0::/48,DIRECT` → Tailscale IPv6 段直连
- `DOMAIN-SUFFIX,tailscale.com,DIRECT` → Tailscale 控制服务器直连
- `DOMAIN-SUFFIX,login.tailscale.com,DIRECT` → Tailscale 认证直连

## 2.2 问题二：MagicDNS 劫持域名解析

Tailscale 默认开启 MagicDNS，将系统 DNS 改为 100.100.100.100，与 ClashX Pro 增强模式的 fake-ip DNS（198.18.0.2）冲突。

典型表现：浏览器能访问 YouTube，但 Claude Code 等终端应用无法连接。

**解决方法：** 关闭 Tailscale DNS 接管。

操作步骤：
1. 点击 Mac 菜单栏 Tailscale 图标
2. 进入 Preferences（偏好设置）
3. 找到 "Use Tailscale DNS Settings" → 关闭

或通过管理后台：浏览器打开 https://login.tailscale.com/admin/dns → 关闭 MagicDNS

## 2.3 订阅更新覆盖问题

ClashX Pro 订阅更新会覆盖配置文件，Tailscale 直连规则会丢失。

**应对：** 每次订阅更新后，重新在 `rules:` 下插入 2.1 中的 5 条规则，然后 ClashX Pro 菜单 → 配置 → 重新加载配置。

备份规则片段方便粘贴：
```
 - PROCESS-NAME,tailscaled,DIRECT
 - IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
 - IP-CIDR6,fd7a:115c:a1e0::/48,DIRECT,no-resolve
 - DOMAIN-SUFFIX,tailscale.com,DIRECT
 - DOMAIN-SUFFIX,login.tailscale.com,DIRECT
```

## 2.4 验证清单

1. 开启 ClashX Pro 增强模式 + 选择代理节点
2. 开启 Tailscale
3. 浏览器访问 YouTube → VPN 正常
4. 终端 `ssh zhangwenqi@100.123.82.91` → Tailscale SSH 正常
5. Claude Code 对话 → DNS 解析正常

失败排查：
- 第 3 步失败 → 检查代理节点
- 第 4 步失败 → 检查 rules 中 Tailscale 直连规则
- 第 5 步失败 → 检查 Tailscale DNS Settings 是否已关闭

---

# 第三部分：Linux 虚拟机环境配置

## 3.1 Tailscale 安装

```bash
# 安装
curl -fsSL https://tailscale.com/install.sh | sh

# 启动并认证（浏览器打开输出的链接登录）
sudo tailscale up

# 查看分配的 IP
tailscale ip -4
```

## 3.2 SSH 服务

```bash
# 安装（如未安装）
sudo apt update && sudo apt install openssh-server -y

# 启动
sudo systemctl enable --now ssh

# 检查状态
sudo systemctl status ssh
```

## 3.3 Docker 安装

```bash
# 添加 Docker 清华镜像源
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu focal stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 安装
sudo apt-get update -y
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 当前用户加入 docker 组（免 sudo）
sudo usermod -aG docker $USER
# 需要重新登录生效
```

## 3.4 Docker 镜像加速（国内必须配置）

Docker Hub 在国内无法直接访问，需要配置镜像加速器：

```bash
sudo tee /etc/docker/daemon.json > /dev/null << 'EOF'
{
  "registry-mirrors": [
    "https://docker.1ms.run",
    "https://docker.xuanyuan.me",
    "https://docker.m.daocloud.io"
  ]
}
EOF
sudo systemctl daemon-reload && sudo systemctl restart docker
```

注意：镜像加速器地址可能失效，届时搜索"Docker 镜像加速 2026"获取最新可用地址。

## 3.5 构建 Android 12 编译镜像

Dockerfile 位于虚拟机 `~/Radxa_CM5/docker/Dockerfile`，内容基于 Radxa 官方文档，使用清华镜像源。

```bash
cd ~/Radxa_CM5/docker
sudo docker build -t android-builder:12.x .
```

构建完成验证：
```bash
sudo docker run --rm android-builder:12.x bash -c \
  'java -version 2>&1; python3 --version; gcc --version | head -1; which repo'
```

预期输出：
- Java: openjdk 1.8.0
- Python: 3.8.x
- GCC: 9.4.0
- Repo: /usr/local/bin/repo

### 与官方 Dockerfile 的差异（重要）

当前镜像**缺少官方 Dockerfile 中的 5 套 Linaro 交叉编译工具链 COPY**：

```
gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf   → /opt/ 和 /opt/toolchains/
gcc-linaro-aarch64-none-elf-4.8-2013.11_linux          → /opt/ 和 /opt/toolchains/
gcc-arm-none-eabi-6-2017-q2-update                     → /opt/toolchains/
gcc-linaro-6.3.1-2017.02-x86_64_aarch64-linux-gnu      → /opt/toolchains/
gcc-linaro-7.2.1-2017.11-x86_64_aarch64-elf            → /opt/toolchains/
```

**原因：** 这些工具链需要单独下载（官方文档未提供下载地址），放到 Dockerfile 同目录才能 COPY。

**影响评估：**
- Radxa Android SDK 的 `prebuilts/` 目录自带编译所需工具链
- 编译脚本 `build.sh` 优先使用 SDK 内的工具链
- 这些 Linaro 工具链主要给 U-Boot 和 kernel **单独编译**用

**编译时如果报错找不到工具链，按以下方式排查修复：**

1. 确认报错中缺少的具体工具链名称
2. 搜索 Linaro 官网下载对应版本：https://releases.linaro.org/components/toolchain/binaries/
3. 解压到 Docker 容器的 `/opt/toolchains/` 目录：
   ```bash
   # 在容器内
   cd /opt/toolchains
   wget <工具链下载地址>
   tar -xf <工具链压缩包>
   ```
4. 或者将工具链放到 `~/Radxa_CM5/docker/` 目录，取消 Dockerfile 中 COPY 行注释后重新构建镜像

## 3.6 Radxa CM5 Android 12 编译环境（基于 Ubuntu 20.04）

### 3.6.1 解压 SDK

```bash
cd ~/Radxa_CM5/SDK_Android
tar -xzf radxa_android_sdk_backup.tar.gz
# 解压后目录为 radxa-android-sdk/，耗时视磁盘速度而定
```

### 3.6.2 编译前必须修复的问题（重要）

SDK 存在两个已知问题，必须在编译前修复，否则编译会失败。

**问题一：GMS 引用不存在（lunch 阶段报错）**

错误信息：`vendor/partner_gms/products/gms.mk does not exist`

原因：RadxaCM5.mk 引用了 Google GMS 包（Google Play 服务），但开源 SDK 不包含 GMS（需 Google 授权）。

修复：注释掉 GMS 引用行。

```bash
cd ~/Radxa_CM5/SDK_Android/radxa-android-sdk
sed -i 's/$(call inherit-product, vendor\/partner_gms\/products\/gms.mk)/#$(call inherit-product, vendor\/partner_gms\/products\/gms.mk)/' device/rockchip/rk3588/RadxaCM5/RadxaCM5.mk
```

验证：
```bash
grep "gms" device/rockchip/rk3588/RadxaCM5/RadxaCM5.mk
# 应看到该行前面有 # 注释符
```

注意：注释 GMS 后编译出的系统不含 Google Play 商店和 Google 服务，其他功能不受影响。

**问题二：缺少 RadxaCM5.config 内核配置文件（kernel 编译阶段报错）**

错误信息：`No configuration exists for this target on this architecture - RadxaCM5.config`

原因：BoardConfig.mk 指定 `PRODUCT_KERNEL_CONFIG := rockchip_defconfig android-11.config RadxaCM5.config`，但 `kernel-5.10/kernel/configs/` 目录下没有 `RadxaCM5.config` 文件。

修复：参考 rock5b.config 创建 CM5 内核配置文件。

```bash
cat > kernel-5.10/kernel/configs/RadxaCM5.config << 'EOF'
CONFIG_SND_SOC_ES8316=y
CONFIG_RTL8852BE=m
CONFIG_BT_RTKBTUSB=y
CONFIG_SENSORS_PWM_FAN=y
CONFIG_TOUCHSCREEN_GT9XX=y
CONFIG_TOUCHSCREEN_FTS=y
CONFIG_TOUCHSCREEN_FTS_RADXA=y
CONFIG_DRM_PANEL_RADXA=y
CONFIG_DRM_PANEL_RASPBERRYPI_TC358762=y
CONFIG_ROCKPI_MCU=y
CONFIG_VIDEO_IMX219=y
EOF
```

### 3.6.3 编译前增大 Swap（防止 OOM，重要）

Android 全量编译内存峰值可超过 20GB，虚拟机默认 Swap 仅 1GB，编译到 99% 时会因内存不足被 OOM Killer 杀死进程。

```bash
# 创建 8GB Swap 文件
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# 写入 fstab 永久生效
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# 验证（应显示 9GB 左右 Swap）
free -h | grep -i swap
```

同时建议 VMware 虚拟机内存分配 **16GB 以上**（20GB+ 更佳）。

### 3.6.4 进入 Docker 编译环境

```bash
# 注意挂载的是解压后的 radxa-android-sdk 目录
sudo docker run -it \
  --name android-build \
  -v ~/Radxa_CM5/SDK_Android/radxa-android-sdk:/workspace \
  -w /workspace \
  android-builder:12.x \
  bash
```

### 3.6.5 执行编译

```bash
# 在 Docker 容器内执行
source build/envsetup.sh
lunch RadxaCM5-userdebug

# 方法一：全量编译（推荐，约 5 小时）
# 如果虚拟机内存 ≤ 20GB，建议用 -j8 限制线程数减少内存峰值
./build.sh -AUCKup

# 方法二：如果 build.sh 因 OOM 中途失败，用 make 继续（会复用已编译缓存）
make -j8
```

编译参数说明：`-A` AOSP、`-U` U-Boot、`-C` Config、`-K` Kernel、`-u` update.img、`-p` 打包

编译过程中 `Build sandboxing disabled due to nsjail error` 是正常提示，Docker 容器内不支持 nsjail 沙箱，不影响编译。

### 3.6.6 编译后打包镜像

如果使用 `make -j8` 单独编译 AOSP（非 build.sh），需要手动打包：

```bash
# 在容器内执行
./mkimage.sh
```

### 3.6.7 验证编译结果

```bash
ls -lh rockdev/Image-RadxaCM5/
```

预期镜像文件：

| 文件 | 说明 |
|------|------|
| MiniLoaderAll.bin | Bootloader 加载器 |
| idbloader.img | IDB Loader |
| uboot.img | U-Boot |
| boot.img | Boot 镜像（内核+ramdisk） |
| recovery.img | Recovery 模式镜像 |
| super.img | 系统分区（system+vendor+odm，约 1.6GB） |
| dtbo.img | 设备树 Overlay |
| resource.img | 内核资源 |

### 3.6.8 退出和重新进入容器

```bash
# 退出容器
exit

# 重新进入已创建的容器
sudo docker start -ai android-build
```

### 3.6.9 烧录到开发板

编译完成的镜像在虚拟机 `~/Radxa_CM5/SDK_Android/radxa-android-sdk/rockdev/Image-RadxaCM5/` 目录。

拿到 CM5 开发板后：
1. 将镜像文件拷贝到 Windows
2. 使用 **RKDevTool**（瑞芯微官方烧录工具）烧录
3. 开发板进入 Loader 模式（按住 Recovery 按键上电）
4. 在 RKDevTool 中加载各 img 文件烧录

## 3.7 OrangePi CM5 Base Tablet Android 13 编译环境（基于 Ubuntu 22.04 Docker）

OrangePi CM5 核心板 + **CM5 Base Tablet** 底板（RK3588S）使用 Android 13 源码，
与 Radxa CM5 的 Android 12 并行存在。
为避免污染主机环境，使用独立的 Docker 镜像（基于 Ubuntu 22.04，对齐 OrangePi 官方推荐环境）。

### 3.7.0 板子型号选择（关键）⚠️

**本项目板子 = CM5 核心板 + CM5 Base Tablet 底板**（带板载 AP6256 WiFi/BT、M.2 SSD、DP over Type-C、MIPI 屏接口、电池充电等）。

**不是 CM5 Base 底板**（Base 底板有 3 个 RJ45 网口但无 WiFi，硬件差异巨大）。

`make.sh` 内的 board 映射必须**严格对应**底板型号，否则会烧出配置错误的固件：

| 实际底板 | `-b` 参数 | 产物目录 | kernel DTS |
|---|---|---|---|
| **CM5 Base Tablet（本项目）** | **`orangepicm5-tablet`** | **`rockdev/Image-rk3588s_s/`** | `rk3588s-orangepi-cm5-tablet.dts` |
| CM5 Base（只有网口/无 WiFi） | `orangepicm5` | `rockdev/Image-rk3588s_t/` | `rk3588s-orangepi-cm5.dts` |
| CM5 Base Tablet + MIPI LCD | `orangepicm5-tablet` + `export build_lcd_image=true` | `rockdev/Image-rk3588s_s/` | `rk3588s-orangepi-cm5-tablet-lcd.dts` |

**常见误判**：
- 编 `orangepicm5` 产物目录是 `rk3588s_t`（注意是 `_t` 不是 Tablet 的意思）
- 编 `orangepicm5-tablet` 产物目录反而是 `rk3588s_s`
- 命名完全反直觉，**以 `make.sh` 里的 case 映射为准**

### 3.7.1 官方参考文档

- 《OrangePi_CM5_Base_Tablet_RK3588S_用户手册_v1.0.pdf》第 7 章：Android 13 源码的编译方法

### 3.7.2 SDK 文件准备

OrangePi Android 13 SDK 来自官方百度云/谷歌网盘，是 9 个分卷压缩包：

```
Android_13.tar.gz00  (4.0GB)
Android_13.tar.gz01  (4.0GB)
...
Android_13.tar.gz08  (1.9GB)
md5sum              （校验文件）
```

存放路径：`~/OrangePi_CM5/Aandroid_OrangePi/`

### 3.7.3 MD5 校验

```bash
cd ~/OrangePi_CM5/Aandroid_OrangePi
md5sum -c md5sum
# 9 个文件全部显示"成功/确定"才能继续
```

### 3.7.4 合并压缩包（重要）

**经实测：官方文档的管道流式解压（`cat ... | tar -xzf -`）会在某个分卷边界出错**
（已解压 46GB 后报 `gzip: invalid compressed data--format violated`）。

**推荐做法：先合并成单个文件，再解压。**

```bash
cd ~/OrangePi_CM5/Aandroid_OrangePi
# 显式指定顺序合并（避免通配符顺序问题）
cat Android_13.tar.gz00 Android_13.tar.gz01 Android_13.tar.gz02 \
    Android_13.tar.gz03 Android_13.tar.gz04 Android_13.tar.gz05 \
    Android_13.tar.gz06 Android_13.tar.gz07 Android_13.tar.gz08 \
    > Android_13.tar.gz

# 合并后约 34GB，耗时约 15 分钟（SSD 速度约 37MB/s）
ls -lh Android_13.tar.gz
```

### 3.7.5 解压源码

```bash
# 解压到当前目录，产生 Android_13/ 目录
tar -xzf Android_13.tar.gz

# 解压后预计 80-100GB，耗时约 20-30 分钟
du -sh Android_13/
```

解压成功后可以删除合并文件和分卷文件节省空间：
```bash
# 可选：保留分卷和 md5sum 作为备份源，删除合并文件
rm Android_13.tar.gz

# 或：已验证解压成功后，彻底清理（需确认 Android_13/ 完整）
# rm Android_13.tar.gz Android_13.tar.gz0*
```

### 3.7.6 创建 Dockerfile（含所有踩坑补丁，实测可用版）

Dockerfile 位于虚拟机 `~/OrangePi_CM5/docker/Dockerfile`，基于 Ubuntu 22.04。
官方文档 7.2 的依赖列表不完整，本版本已加入所有实际踩坑后补齐的包。

```dockerfile
# OrangePi CM5 Android 13 编译环境
# 基础镜像：Ubuntu 22.04（符合 OrangePi 官方推荐环境）
FROM ubuntu:22.04

# 清华镜像源
RUN sed -i 's@archive.ubuntu.com@mirrors.tuna.tsinghua.edu.cn@g; \
    s@security.ubuntu.com@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.list

ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# 启用 i386 多架构（Android 编译需要 32 位兼容库）
RUN dpkg --add-architecture i386

# 官方文档 7.2 要求的编译依赖
RUN apt-get update -y && apt-get install -y \
    git gnupg flex bison gperf build-essential \
    zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 \
    lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev ccache \
    libgl1-mesa-dev libxml2-utils xsltproc unzip \
    && apt-get install -y u-boot-tools

# Android 13 需要 OpenJDK 11
RUN apt-get install -y openjdk-11-jdk

# 【踩坑 1】Ubuntu 22.04 默认没有 libncurses.so.5
# 但 Android 预编译 Clang 工具链依赖它，不装会导致编译到 40+ 分钟时报错
RUN apt-get install -y libncurses5 libncurses5:i386

# 【踩坑 2】Ubuntu 22.04 编译 Android 可能需要的预防性包
RUN apt-get install -y \
    libssl-dev pkg-config m4 imagemagick \
    python-is-python3 python3-protobuf \
    protobuf-compiler libprotobuf-dev \
    libncurses-dev

# 【踩坑 3】Rockchip U-Boot 的 make.sh 硬依赖 Python 2
# 不装会导致 MiniLoaderAll 合成阶段报 "ERROR: No python2"
RUN apt-get install -y python2 python2-dev python2.7

# 辅助工具
RUN apt-get install -y python3 python3-pip rsync cpio bc kmod \
    device-tree-compiler liblz4-tool lzop sudo vim nano \
    fontconfig schedtool

RUN apt-get clean && rm -rf /var/lib/apt/lists/*

WORKDIR /workspace
CMD ["/bin/bash"]
```

### 3.7.7 构建 Docker 镜像

```bash
cd ~/OrangePi_CM5/docker
sudo docker build -t android-builder-orangepi:13.x .
# 耗时约 5-10 分钟
```

验证镜像：
```bash
sudo docker images android-builder-orangepi
# 预期：TAG=13.x, SIZE 约 2GB
```

### 3.7.8 进入 Docker 编译环境

```bash
sudo docker run -it \
  --name orangepi-build \
  -v ~/OrangePi_CM5/Aandroid_OrangePi/Android_13:/workspace \
  -w /workspace \
  android-builder-orangepi:13.x \
  bash
```

### 3.7.9 执行编译

按官方文档 7.2 节编译参数：

| 参数 | 作用 |
|------|------|
| `-B` | 编译 U-Boot |
| `-K` | 编译 Kernel |
| `-a` | 编译 Android |
| `-F` | 编译 U-Boot + Kernel + Android（三合一） |
| `-M` | 在 rockdev 目录生成分区镜像 |
| `-u` | 打包生成最终 update.img |
| `-b` | 指定开发板型号（本项目必须用 `orangepicm5-tablet`，详见 3.7.0） |
| `--gapps` | 包含 Google Play 服务（GApps），**不需要就不加**（编译更快、镜像更干净） |

推荐全量编译命令（本项目默认配置）：
```bash
# 容器内或后台 docker run -d 执行
./make.sh -FMu -b orangepicm5-tablet
# 如需 Google Play：末尾加 --gapps
# 如接 MIPI LCD 屏：前面 export build_lcd_image=true
```

实测耗时：
- **首次 Base 编译：6 小时 11 分钟**（含全量下载 + 编译 + 打包）
- **切换到 Tablet 重编：预期 2~4 小时**（Android 源码解析要重来，kernel/uboot 部分可复用缓存）

推荐后台执行（避免 SSH 断开中断）：
```bash
sudo docker run -d --name orangepi-tablet-build \
  -v /home/zhangwenqi/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13:/workspace \
  -w /workspace \
  android-builder-orangepi:13.x \
  bash -c 'cd /workspace && \
    ./make.sh -FMu -b orangepicm5-tablet 2>&1 | tee /workspace/tablet-build.log'

# 查看进度（每 3 秒刷新）
watch -n 3 "ssh linux-vm 'tail -5 ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/tablet-build.log'"

# 或实时跟随
ssh linux-vm 'tail -f ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/tablet-build.log'

# 监控磁盘/内存
ssh linux-vm 'docker exec orangepi-tablet-build du -sh /workspace/out/'
```

### 3.7.10 编译后的 U-Boot 修复（实测必需）

**`-F` 参数理论上会编译 U-Boot，但实际只产出 `u-boot.img` 等基础文件，
不会自动调用 Rockchip 脚本合成 `MiniLoaderAll.bin` 和 `trust.img`。**

如果 `make.sh -FMu` 最后日志中出现：
```
u-boot/trust.img not fount! Please make it from u-boot first!
u-boot/*MiniLoaderAll_*.bin not fount! Please make it from u-boot first!
Error:<AddFile> open file failed,err=2!
Make firmware FAILED
```

需要手动执行两步修复：

**修复步骤 1：合成 Rockchip U-Boot（生成 MiniLoaderAll + uboot.img 新版）**

```bash
# 启动临时容器执行 u-boot/make.sh rk3588
sudo docker run --rm \
  -v /home/zhangwenqi/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13:/workspace \
  -w /workspace/u-boot \
  android-builder-orangepi:13.x \
  ./make.sh rk3588
```

成功标志：
```
Image(no-signed, version=0): uboot.img (FIT with uboot, trust...) is ready
Image(no-signed): rk3588_spl_loader_v1.13.xxx.bin (with spl, ddr...) is ready
```

**修复步骤 2：创建 trust.img 符号文件（新版 Rockchip 已将 trust 合并到 uboot.img）**

```bash
# 在虚拟机宿主机执行（非容器内）
cd ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/u-boot/
sudo cp -a uboot.img trust.img
```

**修复步骤 3：重新打包（跳过编译，只做 mkimage + update.img）**

```bash
sudo docker run --rm \
  -v /home/zhangwenqi/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13:/workspace \
  -w /workspace \
  android-builder-orangepi:13.x \
  ./make.sh -Mu -b orangepicm5-tablet
```

`-Mu` 只做"生成分区镜像 + 打包 update.img"，不重新编译 Android，耗时约 5-10 分钟。

### 3.7.11 验证编译结果

成功后在 `rockdev/Image-rk3588s_s/` 目录生成所有镜像：

```bash
ls -lh ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/rockdev/Image-rk3588s_s/
```

预期产物（实测）：

| 文件 | 大小 | 分区/作用 |
|------|------|----------|
| **update.img** | **2.3GB** | eMMC/TF 卡完整固件（首要烧录目标） |
| **update_spi_nvme.img** | **2.3GB** | SPI Flash + NVMe SSD 组合烧录 |
| MiniLoaderAll.bin | 465KB | bootloader 加载器（MaskROM 引导） |
| uboot.img | 4.0MB | U-Boot（已内含 trust） |
| trust.img | 4.0MB | Trust 镜像（副本，兼容脚本） |
| boot.img | 37MB | 内核 + ramdisk（boot 分区） |
| recovery.img | 47MB | Recovery 模式镜像 |
| super.img | 2.2GB | 超级分区（含 system+vendor+odm+product） |
| dtbo.img | 443B | 设备树 Overlay |
| resource.img | 2.5MB | 内核资源（开机动画、字体等） |
| vbmeta.img | 4KB | 验证启动元数据 |
| misc.img | 48KB | 其他信息分区 |
| baseparameter.img | 1MB | 显示屏参数 |
| parameter.txt | - | 分区表定义 |

### 3.7.12 烧录到 OrangePi CM5 开发板

#### 首次烧录：只需 update.img

**`update.img` 是完整的 Android 13 系统固件**，包含所有分区镜像。
烧录 update.img 会一次性写入所有分区，等于给开发板全新刷系统。

烧录步骤：
1. 从虚拟机拷贝 `update.img`（2.3GB）到 Windows 电脑：
   ```bash
   # Mac 或虚拟机上执行（假设通过 Tailscale SSH）
   scp zhangwenqi@100.123.82.91:/home/zhangwenqi/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/rockdev/Image-rk3588s_s/update.img ~/Desktop/
   ```

2. Windows 电脑下载并安装 **RKDevTool**（瑞芯微官方烧录工具）

3. OrangePi CM5 进入 **MaskROM 模式**：
   - 按住板子上的 **Recovery / MaskROM 按键**
   - 插入 USB 线到电脑
   - 松开按键

4. RKDevTool 界面切换到 **升级固件** 标签

5. 点击 **固件** 按钮，选择 `update.img`

6. 点击 **升级** 按钮，等待烧录完成（约 2-5 分钟）

7. 烧录完成后开发板自动重启进入 Android 13

#### 镜像文件说明与高级烧录

`update.img` = Rockchip 打包格式，内部已包含所有其他 `.img` 文件。
单独的 `.img` 文件是给**开发调试**用的，用途：

| 场景 | 用哪个镜像 | 烧录方式 |
|------|-----------|---------|
| 首次刷机 / 完整升级 | `update.img` | RKDevTool 升级固件 |
| 只调试内核 | `boot.img` | `fastboot flash boot boot.img` |
| 只更新系统 | `super.img` | `fastboot flash super super.img` |
| 只更新 U-Boot | `uboot.img` | `fastboot flash uboot uboot.img` |
| 只刷 Recovery | `recovery.img` | `fastboot flash recovery recovery.img` |
| 改开机画面 | `resource.img` | `fastboot flash resource resource.img` |
| SPI Flash + NVMe 设备 | `update_spi_nvme.img` | RKDevTool 升级固件 |

**日常开发推荐**：
- 改内核 / 驱动 → 单独烧 `boot.img`（几秒到 1 分钟）
- 改应用 / 系统框架 → 单独烧 `super.img`（1-2 分钟）
- 改 Bootloader → 单独烧 `uboot.img` + `MiniLoaderAll.bin`
- 不确定或要彻底刷新 → 烧 `update.img`（2-5 分钟）

这样可以避免每次修改都重刷整个系统。

### 3.7.12 关键差异对比（与 Radxa CM5）

| 项目 | Radxa CM5 | OrangePi CM5 |
|------|-----------|--------------|
| Android 版本 | 12 | 13 |
| Docker 基础镜像 | ubuntu:20.04 | **ubuntu:22.04** |
| JDK 版本 | OpenJDK 8 | **OpenJDK 11** |
| 交叉编译工具链 | 需手动 COPY 5 套 Linaro | 编译脚本自动下载 |
| SDK 分发方式 | 单个压缩包 | 9 个分卷（需先合并） |
| 编译脚本 | `./build.sh -AUCKup` | `./make.sh -FMu -b orangepicm5-tablet` |
| 镜像输出目录 | `rockdev/Image-RadxaCM5/` | `rockdev/Image-rk3588s_s/` |
| GMS 支持 | 需注释（官方不含） | `--gapps` 参数可选（默认不启用） |
| Docker 镜像名 | android-builder:12.x | android-builder-orangepi:13.x |

### 3.7.13 容器管理

```bash
# 退出容器（编译中断）
exit

# 重新进入已创建的容器（保留编译缓存）
sudo docker start -ai orangepi-build

# 删除容器（重新开始，会丢失容器内临时修改）
sudo docker rm -f orangepi-build
```

### 3.7.14 踩坑速查表（完整编译流水线）

以下所有坑都是实际编译过程中踩到的，新电脑按文档 3.7.1~3.7.12 操作时**严格按顺序**可以避开。

| # | 阶段 | 现象 | 根因 | 解决方法 |
|---|------|------|------|----------|
| 1 | SDK 校验 | gz03 分卷 MD5 校验通过但解压到 46GB 报 `invalid compressed data` | 百度网盘下载过程中该分卷损坏，但上游 md5sum 正好是损坏版本的 MD5 | 重新从百度网盘下载 gz03，对比同网盘其他用户汇报的 MD5 |
| 2 | SDK 合并 | 官方文档推荐的 `cat ... \| tar -xzf -` 管道式解压在分卷边界处出错 | 管道模式下 tar 无法定位错误位置 | 改用"先合并成单文件 → 再解压"两步法 |
| 3 | 构建镜像 | Android ninja 42 分钟时报 `libncurses.so.5: cannot open shared object file` | Ubuntu 22.04 默认只有 libncurses.so.6 | Dockerfile 装 `libncurses5 libncurses5:i386` |
| 4 | 主体编译 | 6 小时编译完成后打包失败：`u-boot/trust.img not fount` | `make.sh -F` 只跑 U-Boot 编译，没调用 Rockchip 合成脚本 | 手动 `cd u-boot && ./make.sh rk3588` |
| 5 | U-Boot 合成 | Rockchip make.sh 报 `ERROR: No python2` | Ubuntu 22.04 没 Python 2（默认只有 python3） | Dockerfile 装 `python2 python2-dev python2.7` |
| 6 | 打包阶段 | mkimage.sh 找不到 `trust.img` | 新版 Rockchip 把 trust 合并到 uboot.img，不单独生成 | `cp uboot.img trust.img`（副本伪装） |
| 7 | 打包阶段 | 手动 lunch 后执行 mkimage.sh 编译目标变成 `aosp_arm` | OrangePi make.sh 内部根据 `-b orangepicm5-tablet` 设置 lunch，手动设不对 | 直接用 `./make.sh -Mu -b orangepicm5-tablet` 打包 |
| 11 | Kernel 编译 | `fatal error: 'rk3588-aging.dtsi' file not found` 在 tablet.dts:1181 | OrangePi 公开 SDK 包未含 aging.dtsi（老化测试专用），Base DTS 已注释，Tablet DTS 漏注释 | Tablet DTS 第 1181 行改为 `//#include "rk3588-aging.dtsi"`（见 commit `f641b4381`） |
| 12 | Android Soong | `hardware/rockchip/libhwjpeg/Android.bp: module source path ".git/HEAD" does not exist` | SDK 打包时删了 `.git/` 目录，但 Android.bp 的 `genrule` 依赖 `.git/HEAD` 生成版本号 | 解压 SDK 后执行：`for m in libhwjpeg libmpimmz; do mkdir -p hardware/rockchip/$m/.git && echo "ref: refs/heads/main" > hardware/rockchip/$m/.git/HEAD; done`（脚本 genversion.sh 会容错失败，用空版本号继续） |
| 13 | Kernel Config | `kernel_config` 引用的 `sdio_bcmdhd.config` / `android-11.config` / `aw87xx.config` 不存在 | SDK 打包不完整，但 `rockchip_defconfig` 已默认启用 bcmdhd | 三个文件创建空占位：`cd kernel-5.10/arch/arm64/configs && touch sdio_bcmdhd.config android-11.config aw87xx.config`（merge_config 合并空文件不影响） |
| 14 | 板子选择 | 烧录后 WiFi 扫不到、SDIO 报 `could not request pin 101 from group sdiom1-pins` | 编译用 `-b orangepicm5`（Base）但实际硬件是 Base Tablet；Base DTS 启用 gmac1，RGMII bus2 pin 与 SDIO M1 复用，SDIO 拿不到 pin | 改用 `-b orangepicm5-tablet`，Tablet DTS 默认不启用 gmac1，SDIO 正常 |
| 15 | Android 打包 98% | `check_vintf_compatible.log FAILED: For config CONFIG_DEVMEM, value = y but required n` | Android 13 FCM level 6 要求 kernel 禁用 /dev/mem，但 Rockchip `rockchip_defconfig` 默认 `CONFIG_DEVMEM=y`（硬件调试依赖）。该失败导致 super.img 未打包，update.img 只剩 91MB 没有 Android 主体 | 在 `device/rockchip/rk3588/rk3588s_s/BoardConfig.mk` 加 `BOARD_SKIP_VINTF_COMPATIBILITY_CHECK := true`（本开发板不走 OTA，跳过检查）。**切勿** 在 BoardConfig.mk 加 `PRODUCT_*` 变量，见坑 16 |
| 16 | Android lunch | 编译 1m49s 快速失败：`TARGET_PRODUCT=aosp_arm` + `kernel image not fount![/workspace/out/target/product/generic/kernel]` | BoardConfig.mk 里误放 `PRODUCT_OTA_ENFORCE_VINTF_KERNEL_REQUIREMENTS := false` → Android build 系统报 `cannot assign to readonly variable` → `dumpvars failed with: exit status 1` → lunch 失败 → TARGET_PRODUCT fallback 到默认 `aosp_arm` | AOSP 强制 scope：`BOARD_*` 变量只能放 `BoardConfig.mk`，`PRODUCT_*` 变量只能放 `xxx.mk`（product makefile）。需 VINTF 相关 PRODUCT_* 时放 `rk3588s_s.mk`（不是 BoardConfig.mk） |
| 17 | Soong 缓存污染 | 修复 BoardConfig 后重编仍然失败、`TARGET_PRODUCT=aosp_arm`，即使 lunch 命令本身正确 | 上次 lunch 失败后 Soong 把 `aosp_arm` 状态写进了 `out/soong/soong.variables`（`DeviceName: "generic"`），下次 build 读旧状态，即使 make.sh 调对了 lunch 也无效 | 清理 Soong 全局状态但保留 intermediates：`sudo rm -f out/soong/soong.variables out/soong/build.ninja out/combined-*.ninja out/build-*.ninja out/build_date.txt out/build_progress.pb`（注意 Docker 跑的是 root，宿主 zhangwenqi 删需 sudo）。保留 `out/soong/.intermediates/`（53G）和 `out/target/product/rk3588s_s/`（11G）复用编译缓存 |
| 18 | TARGET_PRODUCT 差异 | make.sh 检测到 BoardConfig.mk 变化自动触发 `make installclean`，之后 TARGET_PRODUCT 丢失 | `build_android()` 内部 `make installclean` 可能因为环境不一致 fallback；或者 `installclean` 会清掉 out/target/product/rk3588s_s/ 的部分文件，再次 lunch 时 Soong 对不上 | 尽量避免编译过程中修改 BoardConfig.mk。必须改时：改完后清 Soong 状态（见坑 17），再重编 |
| 8 | 资源管理 | Soong 阶段内存峰值 19GB，会耗 Swap | Android 构建系统需要巨大内存 | 虚拟机分配 **20GB 内存 + 9GB Swap**（已配） |
| 9 | 编译效率 | 编译中断重启后重新跑全部阶段 | 未保留 out/ 缓存 | 不要 `rm -rf out/`，重启时 make.sh 会增量继续 |
| 10 | Docker 默认源 | `docker build` 拉 `ubuntu:22.04` 超时 | Docker Hub 国内访问慢 | `/etc/docker/daemon.json` 配置镜像加速器 |

### 3.7.15 完整编译流水线（新电脑复现）

如果换新电脑后需要重新编译，按以下顺序：

```bash
# === 准备阶段 ===
# 1. 恢复 SDK 分卷到 ~/OrangePi_CM5/Aandroid_OrangePi/archive/
# 2. MD5 校验所有分卷
cd ~/OrangePi_CM5/Aandroid_OrangePi/archive
md5sum -c md5sum

# 3. 合并成单文件到 merged/
cd ..
cat archive/Android_13.tar.gz00 archive/Android_13.tar.gz01 \
    archive/Android_13.tar.gz02 archive/Android_13.tar.gz03 \
    archive/Android_13.tar.gz04 archive/Android_13.tar.gz05 \
    archive/Android_13.tar.gz06 archive/Android_13.tar.gz07 \
    archive/Android_13.tar.gz08 > merged/Android_13.tar.gz

# 4. 解压源码
cd merged && tar -xzf Android_13.tar.gz

# === 编译阶段 ===
# 5. 构建 Docker 镜像（按 3.7.6 的 Dockerfile）
cd ~/OrangePi_CM5/docker
sudo docker build -t android-builder-orangepi:13.x .

# 6. 【前置】修补 SDK 打包残留问题（见 3.7.14 坑 11-13）
cd ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13
# 6.1 Tablet DTS 注释 aging.dtsi（已 commit 到 dev/mac，git pull 即可）
# 6.2 创建 .git/HEAD 占位
for m in libhwjpeg libmpimmz; do
  mkdir -p "hardware/rockchip/$m/.git"
  echo "ref: refs/heads/main" > "hardware/rockchip/$m/.git/HEAD"
done
# 6.3 创建空 kernel config 片段占位
cd kernel-5.10/arch/arm64/configs
touch sdio_bcmdhd.config android-11.config aw87xx.config
cd ../../../..

# 7. 主体编译（后台，约 2-4 小时）
sudo docker run -d --name orangepi-tablet-build \
  -v ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13:/workspace \
  -w /workspace \
  android-builder-orangepi:13.x \
  bash -c 'cd /workspace && \
    ./make.sh -FMu -b orangepicm5-tablet 2>&1 | tee /workspace/tablet-build.log'

# 8. 等主体编译完成（可能报 trust.img 缺失，走步骤 9-11 修复）

# === 修复阶段（若步骤 7 最后打包报 trust.img 缺失则需要） ===
# 9. 手动合成 Rockchip U-Boot
sudo docker run --rm \
  -v ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13:/workspace \
  -w /workspace/u-boot \
  android-builder-orangepi:13.x \
  ./make.sh rk3588

# 10. 创建 trust.img 伪装
sudo cp -a ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/u-boot/uboot.img \
       ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/u-boot/trust.img

# 11. 只打包（复用已编译产物，约 5-10 分钟）
sudo docker run --rm \
  -v ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13:/workspace \
  -w /workspace \
  android-builder-orangepi:13.x \
  ./make.sh -Mu -b orangepicm5-tablet

# === 完成 ===
# 11. 验证 update.img
ls -lh ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/rockdev/Image-rk3588s_s/update.img
# 预期：2.3GB
```

## 3.8 Linux 虚拟机共享 Windows VPN（外网访问）

虚拟机通过 VMware 桥接网络使用 Windows 的 Clash 代理上外网。已配置开机自动生效。

### 当前配置信息（2026-04-15 已验证）

| 项目 | 值 |
|------|-----|
| Windows 局域网 IP | 192.168.6.57 |
| Clash 代理端口 | 7890 |
| 虚拟机代理配置文件 | `/etc/profile.d/proxy.sh` |

### Windows 端配置（一次性）

1. Clash for Windows → 开启 **"允许局域网连接入Clash"**（Allow LAN）
2. Windows 防火墙放行 7890 端口（管理员 PowerShell 执行）：
   ```powershell
   netsh advfirewall firewall add rule name="Clash Proxy" dir=in action=allow protocol=tcp localport=7890
   ```

### Linux 虚拟机端配置（已完成，开机自动生效）

代理配置写入 `/etc/profile.d/proxy.sh`，每次登录自动加载：

```bash
export http_proxy="http://192.168.6.57:7890"
export https_proxy="http://192.168.6.57:7890"
export HTTP_PROXY="http://192.168.6.57:7890"
export HTTPS_PROXY="http://192.168.6.57:7890"
export no_proxy="localhost,127.0.0.1,100.64.0.0/10,192.168.0.0/16"
export NO_PROXY="localhost,127.0.0.1,100.64.0.0/10,192.168.0.0/16"
```

### 验证外网

```bash
curl -I https://www.google.com
curl -I https://releases.linaro.org/
```

### 前提条件

- Windows 电脑**开机且 Clash 运行中**
- Clash 的 "允许局域网连接入Clash" 保持开启

### 注意事项

- **Windows IP 变化**：如果 Windows 通过 DHCP 获取 IP，重启路由器后 IP 可能变化。届时需要修改 `/etc/profile.d/proxy.sh` 中的 IP 地址：
  ```bash
  sudo nano /etc/profile.d/proxy.sh
  # 将 192.168.6.57 替换为新的 Windows IP
  ```
  然后重新登录或执行 `source /etc/profile.d/proxy.sh` 生效。
  建议在 Windows 上设置静态 IP 或在路由器中绑定 DHCP 避免此问题。

- **Docker 容器内使用代理**：`/etc/profile.d/` 的代理对 Docker 容器内不生效，启动容器时需传入环境变量：
  ```bash
  sudo docker run -it \
    -e http_proxy="http://192.168.6.57:7890" \
    -e https_proxy="http://192.168.6.57:7890" \
    -e no_proxy="localhost,127.0.0.1" \
    --name android-build \
    -v ~/Radxa_CM5/SDK_Android:/workspace \
    -w /workspace \
    android-builder:12.x bash
  ```

---

# 第四部分：三端 SSH 互通与文件传输配置

> 本部分覆盖 Mac / Linux 虚拟机 / Windows 三台机器之间的免密 SSH 配置，以及大文件传输的默认策略（Tailscale 直连，无需中转）。
> 4.1~4.5 是 Mac ↔ 虚拟机的基础配置；**4.6~4.8 是 2026-04-24 补齐的三端互通 + 文件传输规范**。

## 4.1 生成 SSH 密钥

```bash
ssh-keygen -t ed25519 -C "mac-to-linux-vm"
# 一路回车使用默认设置
```

## 4.2 部署公钥到虚拟机

```bash
ssh-copy-id zhangwenqi@100.123.82.91
# 输入虚拟机密码，完成后即可免密登录
```

## 4.3 SSH 配置文件

编辑 `~/.ssh/config`：

```
Host linux-vm
    HostName 100.123.82.91
    User zhangwenqi
    Port 22
    ServerAliveInterval 30
    TCPKeepAlive yes
    ConnectTimeout 60
```

之后直接 `ssh linux-vm` 即可连接。

## 4.4 VS Code Remote-SSH（推荐用于驱动开发）

1. VS Code 安装 Remote - SSH 扩展
2. Cmd+Shift+P → "Remote-SSH: Connect to Host"
3. 选择 linux-vm
4. 连接成功后打开文件夹 `/home/zhangwenqi/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13`

Remote-SSH 模式下 VS Code 的后端（vscode-server、扩展、终端）都跑在虚拟机上，Mac 只负责 UI。
编译、git、文件索引全都是虚拟机本地操作，性能和稳定性最佳。

## 4.5 SMB 挂载（轻量文件浏览/编辑，不适合驱动开发）

macOS 原生支持 SMB，通过 Tailscale 直接挂载虚拟机 Samba 共享目录：

**挂载方式**：
1. Finder → `⌘+K`（连接到服务器）
2. 输入：`smb://100.123.82.91`
3. 输入虚拟机用户名/密码
4. 选择要挂载的共享目录，挂载点位于 `/Volumes/<share_name>`

挂载成功后 VS Code 可直接打开挂载目录，像本地文件一样编辑。

### 适用场景（轻量用途）

| 用途 | 可行性 |
|------|--------|
| 临时查看/编辑几个配置文件 | ✅ |
| 快速预览源码结构 | ✅ |
| Finder 里拖文件到 Mac 备份 | ✅ |
| 打开整个 84GB SDK 做索引开发 | ⚠️ 首次索引慢 |
| 跑编译 / 烧录 / git 大仓库操作 | ❌ 必须 SSH 到虚拟机 |
| 让 Claude Code 自动编译调试 | ❌ Mac shell 无 Linux 工具链 |

### SMB 方案关键局限

1. **文件访问 ≠ 执行环境**：SMB 只能让 Mac 看到文件，无法在 Mac 上跑 Android 编译（缺 Rockchip 工具链、Docker、特定 Python/libncurses5 等依赖）
2. **符号链接支持有限**：Android SDK 含大量符号链接，SMB 可能表现异常
3. **Git 操作不稳定**：文件锁语义差异，`git status` 在 SMB 挂载目录上不可靠
4. **文件系统大小写**：macOS 默认大小写不敏感 + Linux 大小写敏感，可能冲突
5. **`.DS_Store` 污染**：Mac Finder 会在挂载目录留下元数据文件，进 git 会碍事

### 结论：两种方案如何选择

- **驱动开发主力**：Remote-SSH（编辑 + 编译 + 调试 + git 全部在虚拟机）
- **辅助场景**：SMB（只看几个文件、拖文件备份）

---

## 4.6 Mac → Windows SSH 免密（烧录机联动）

### 场景

Windows 是烧录机（装瑞芯微开发工具 RKDevTool），Mac 需要直接推送编译产物（如 `boot.img`、`update.img`）、远程启动 adb、查询 USB 设备状态等。不走 Mac 中转能省一次 2.3GB 拷贝。

### Windows 端一次性准备

1. **启用 OpenSSH 服务器**（Windows 10/11 可选功能）：
   - 设置 → 应用 → 可选功能 → 添加功能 → "OpenSSH 服务器" → 安装
   - PowerShell 管理员执行：
     ```powershell
     Start-Service sshd
     Set-Service sshd -StartupType Automatic
     ```
2. **防火墙**：OpenSSH 安装通常自动加 22 端口入站规则，没有则手动加。
3. **确认用户名**（SSH 用的是 Windows 账户名）：`whoami` 查看，本项目账户是 `Admin`（`desktop-o5a0r5n\admin`）。

### Mac 端推公钥（一次性，需输一次 Windows 密码）

⚠️ **Admin 用户属于 Administrators 组，公钥必须放在 `C:\ProgramData\ssh\administrators_authorized_keys`，不是 `~/.ssh/authorized_keys`**。这是 Windows OpenSSH 的强制安全规则（`sshd_config` 的 `Match Group administrators` 指向这个文件）。

推公钥命令（Mac 上执行，通过 stdin 把公钥管道给 PowerShell）：

```bash
# 1. Mac 确保有 ed25519 key（没有就 ssh-keygen 生成）
[ -f ~/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519

# 2. 一次性推公钥到 Windows（输一次 Windows 密码）
cat ~/.ssh/id_ed25519.pub | ssh Admin@<Windows-Tailscale-IP> 'powershell -NoProfile -Command "$key = ($input | Out-String).TrimEnd(); New-Item -ItemType Directory -Force -Path C:\ProgramData\ssh | Out-Null; Add-Content -Path C:\ProgramData\ssh\administrators_authorized_keys -Value $key -Encoding ASCII; icacls.exe C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant Administrators:F /grant SYSTEM:F"'
```

### Mac 端 `~/.ssh/config` 加别名

```
Host windows-pc
    HostName 100.70.150.78
    User Admin
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 30
```

### 测试

```bash
ssh windows-pc 'whoami'
# 应输出 desktop-o5a0r5n\admin（不问密码）
```

### 中文路径处理（关键坑）

Windows OpenSSH 默认 shell 是 cmd，中文路径经 SSH 通道后会被 GBK 误解析（乱码）。处理方式：**PowerShell `-EncodedCommand`（UTF-16LE + Base64）**：

```bash
SCRIPT='Get-ChildItem -LiteralPath "F:\01_ZWQ\01_Linux_Drive\...\01_安卓13补丁（修复WiFi&BLE配置问题）" | Format-Table'
ENCODED=$(printf '%s' "$SCRIPT" | iconv -f UTF-8 -t UTF-16LE | base64 | tr -d '\n')
ssh windows-pc "powershell -NoProfile -EncodedCommand $ENCODED"
```

Base64 编码后是纯 ASCII，完全绕过字符集和 shell 特殊字符（`&`、空格、引号）问题。

### scp 推文件到 Windows

```bash
scp ~/Downloads/boot.img 'windows-pc:F:/01_ZWQ/01_Linux_Drive/.../01_安卓13补丁（修复WiFi&BLE配置问题）/'
# Windows 路径用正斜杠 /，盘符用 X: 格式，中文路径 scp 能处理
```

---

## 4.7 Linux 虚拟机 → Windows SSH 免密（编译产物直推烧录机）

### 场景

编译产物 `update.img`（2.3GB）要推到 Windows 烧录机。**直接从虚拟机推**最快，不经 Mac 中转（省一次 2.3GB 拷贝）。

### 配置流程（Mac 作跳板全自动，无需 Windows 密码）

思路：虚拟机已有 ed25519 key（之前给 GitHub 用），把公钥通过 **已免密的 Mac** 推送到 Windows——Mac 可以无密码 SSH 到 Windows，所以**一次也不用输 Windows 密码**。

```bash
# 在 Mac 上执行这一段即可（自动走 Mac→虚拟机 + Mac→Windows 两条免密）

# 1. 取虚拟机公钥
VM_PUBKEY=$(ssh linux-vm 'cat ~/.ssh/id_ed25519.pub')

# 2. 通过 Mac 已免密连接 windows-pc，把虚拟机公钥追加到 administrators_authorized_keys
SCRIPT="\$key = '$VM_PUBKEY'; \$auth = 'C:\\ProgramData\\ssh\\administrators_authorized_keys'; if ((Get-Content \$auth -ErrorAction SilentlyContinue) -notmatch [regex]::Escape(\$key)) { Add-Content -Path \$auth -Value \$key -Encoding ASCII; Write-Output 'APPENDED' } else { Write-Output 'ALREADY_PRESENT' }; icacls.exe \$auth /inheritance:r /grant 'Administrators:F' /grant 'SYSTEM:F' | Out-Null"
ENCODED=$(printf '%s' "$SCRIPT" | iconv -f UTF-8 -t UTF-16LE | base64 | tr -d '\n')
ssh windows-pc "powershell -NoProfile -EncodedCommand $ENCODED"

# 3. 虚拟机上配置别名
ssh linux-vm '
cat >> ~/.ssh/config << "EOF"

Host windows-pc
    HostName 100.70.150.78
    User Admin
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 30
    StrictHostKeyChecking accept-new
EOF
chmod 600 ~/.ssh/config
ssh-keyscan -H 100.70.150.78 >> ~/.ssh/known_hosts 2>/dev/null
'

# 4. 测试
ssh linux-vm 'ssh windows-pc "whoami"'
# 应输出 desktop-o5a0r5n\admin
```

### 虚拟机推文件到 Windows（一条命令）

```bash
ssh linux-vm 'scp /home/zhangwenqi/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/rockdev/Image-rk3588s_s/update.img windows-pc:"F:/01_ZWQ/01_Linux_Drive/05_SDK_Dev/03_OrangePi_img镜像和补丁文件/01_安卓13补丁（修复WiFi&BLE配置问题）/"'
```

传输走 Tailscale WireGuard 直连（如果同一 LAN，P2P 直连不计 Tailscale relay 流量）。

---

## 4.8 三端拓扑总览 + 文件传输默认策略

### 三端免密互通拓扑（当前状态）

```
      Mac (rdzleomac-studio, 100.77.198.124)
       ├── ssh linux-vm   → 虚拟机（4.1~4.3）
       └── ssh windows-pc → Windows（4.6）
                │
      虚拟机 (linux-vm, 100.123.82.91)
       ├── ssh windows-pc → Windows（4.7）
       └── ssh github.com:443 → GitHub（第八部分）
                │
      Windows (windows-pc, 100.70.150.78)
       └── （被动端：被 Mac 和虚拟机 SSH 进入）
```

三对链路全部免密 ed25519，**两两可直连互传，不需要中转**。

### 文件传输默认策略

**规则 1：直传优先，避免中转**

| 场景 | 默认命令 |
|---|---|
| Mac 改的源码 → 虚拟机（迭代） | `scp 文件 linux-vm:~/...` |
| 编译产物（虚拟机） → Windows 烧录机 | `ssh linux-vm 'scp update.img windows-pc:/F:/...'` |
| Windows 烧录完成的日志 → Mac（少量） | `scp windows-pc:/F:/xxx.log ~/` |
| Mac 文档改动 → GitHub | `git push`（走 HTTPS + ClashX） |

**不走中转**：`scp linux-vm:update.img ~/Downloads/ && scp ~/Downloads/update.img windows-pc:/F:/` 这种"虚拟机→Mac→Windows"的两段式传输 = 浪费时间 + 污染 Mac 磁盘。除非 Mac 真的需要这份文件。

**规则 2：MD5 校验只对两端（源 + 目的）**

```bash
# 源端
SRC_MD5=$(ssh linux-vm 'md5sum /path/update.img' | awk '{print $1}')

# 目的端（Windows 用 -EncodedCommand 处理中文路径）
SCRIPT='(Get-FileHash -LiteralPath "F:\...\update.img" -Algorithm MD5).Hash.ToLower()'
ENCODED=$(printf '%s' "$SCRIPT" | iconv -f UTF-8 -t UTF-16LE | base64 | tr -d '\n')
DST_MD5=$(ssh windows-pc "powershell -NoProfile -EncodedCommand $ENCODED" | grep -oE '[0-9a-f]{32}' | head -1)

[ "$SRC_MD5" = "$DST_MD5" ] && echo "✅ 一致" || echo "❌ 不一致"
```

**不做三端（源 + 通道 + 目的）校验**：通道节点（如 Mac）不对数据做任何修改，加多一次 MD5 计算（2.3GB 约 10-30 秒）纯属浪费。

### SSH vs SMB 如何选（2026-04-24 决策）

| 场景 | 协议 |
|---|---|
| Claude 自动化脚本（所有批处理） | **SSH/scp**（已配好，一条命令，脚本友好） |
| 用户手动偶尔操作（Windows 资源管理器拖拽、在 Mac Finder 浏览 Linux 源码） | **SMB**（更直观） |
| 大文件首次传输 | SSH（避免 SMB 挂载掉线、凭证复杂） |

**不为 SMB 配免密自动化**——SSH 已满足全部自动化需求，再配一套 Samba 凭证是"为了换而换"，没有实际收益。

### 坑点速查

| 现象 | 根因 | 修复 |
|---|---|---|
| `ssh Admin@windows-pc` 提示 `Permission denied (publickey)` 即使推了公钥到 `~/.ssh/authorized_keys` | Admin 用户属于 Administrators 组，Windows OpenSSH 要求改用 `C:\ProgramData\ssh\administrators_authorized_keys` | 见 4.6，把公钥加到那个文件并设 `icacls` 权限 |
| SSH 到 Windows 中文路径乱码 | 默认 shell 是 cmd，字符集 GBK ≠ Mac UTF-8 | 用 PowerShell `-EncodedCommand`（UTF-16LE + Base64）绕过 |
| SSH 到 Windows 每次看到 `daemon not running; starting now at tcp:5037` | Windows OpenSSH 的 session 关闭后子进程被杀，adb daemon 不持久 | 正常现象，不影响 adb 识别设备 |
| 虚拟机 → Windows 首次 `scp` 报 `Host key verification failed` | 虚拟机 `~/.ssh/known_hosts` 没 Windows 的 host key | `ssh-keyscan -H <Windows-IP> >> ~/.ssh/known_hosts` 预加 |
| Tailscale 流量消耗担心 | Mac/VM/Windows 同一 LAN 下 Tailscale P2P 直连，**不走 DERP relay，不计 100GB/月额度** | `tailscale status` 看到 `direct <IP>:<port>` 就是直连 |

两者不冲突，可以同时配好，按需使用。

---

# 第五部分：换新电脑配置指南

## 5.1 虚拟机备份说明

### 当前备份状态（2026-04-16）

| 项目 | 状态 |
|------|------|
| 备份格式 | OVF（.ovf + .vmdk + .mf 三个文件） |
| 备份内容 | 系统 + Docker（不含 SDK，不含工具链 COPY 到镜像） |
| Docker 镜像 | android-builder:12.x 基础环境已装，**未对齐官方工具链** |
| SDK 压缩包 | 单独存放在 Windows 电脑/移动硬盘 |

### OVF 备份包含的设置

| 配置项 | 是否备份 | 导入后能否修改 |
|--------|---------|---------------|
| 硬盘大小（700GB） | 包含 | 可扩大，不可缩小 |
| 内存大小 | 包含 | 可自由调整 |
| CPU 核心数 | 包含 | 可自由调整 |
| 网络适配器类型（桥接） | 包含 | 可改为 NAT/桥接/仅主机 |
| 操作系统和所有软件 | 包含 | — |

三个文件必须放在同一文件夹，缺一不可：
- `.ovf` — 虚拟机配置描述（CPU、内存、网卡等）
- `.vmdk` — 虚拟磁盘数据（系统和所有文件）
- `.mf` — 校验文件（验证完整性）

## 5.2 新电脑是 Windows — 完整恢复步骤

### 步骤 1：导入虚拟机

1. 新电脑安装 **VMware Workstation 17**（或更高版本）
2. VMware → 文件 → **打开** → 选择 `.ovf` 文件
3. 选择虚拟机存放位置（建议放在 SSD 上）
4. 等待导入完成
5. （可选）调整虚拟机参数：右键虚拟机 → 设置
   - 内存：建议 16GB+（Android 编译非常吃内存）
   - CPU：建议分配 8 核+
   - 网络：选择**桥接模式**（与宿主机同网段）

### 步骤 2：启动虚拟机并恢复网络

1. 启动虚拟机，登录（用户: zhangwenqi）

2. 检查网络：
   ```bash
   ip addr show    # 确认获取到 IP
   ping 8.8.8.8    # 测试内网连通性
   ```

3. 恢复 Tailscale：
   ```bash
   sudo systemctl status tailscaled    # 检查 Tailscale 服务
   sudo tailscale up                   # 重新认证（可能需要）
   tailscale ip -4                     # 记录新的 Tailscale IP
   ```

4. 更新代理配置（Windows IP 可能变化）：
   ```bash
   # 在 Windows cmd 执行 ipconfig 获取新的局域网 IP
   # 然后更新虚拟机代理配置
   sudo nano /etc/profile.d/proxy.sh
   # 将 192.168.6.57 替换为新 Windows 的局域网 IP
   source /etc/profile.d/proxy.sh
   
   # 测试外网
   curl -I https://www.google.com
   ```

### 步骤 3：恢复 SDK

```bash
# 将 SDK 压缩包从移动硬盘拷贝到虚拟机
# 可以通过 VMware 共享文件夹、scp、或 USB 挂载
cd ~/Radxa_CM5/SDK_Android/
# 确认文件
ls -lh radxa_android_sdk_backup.tar.gz
# 校验 MD5
md5sum radxa_android_sdk_backup.tar.gz
```

### 步骤 4：对齐官方 Dockerfile 环境（重要）

备份的虚拟机中 Docker 镜像是基础版本，**缺少 5 套 Linaro 交叉编译工具链**。需要重新下载工具链并重建镜像。

#### 4.1 确认 Docker 和基础镜像正常

```bash
docker images android-builder
# 预期看到 android-builder:12.x 镜像
```

#### 4.2 确认代理可用（下载工具链需要外网）

```bash
curl -I https://releases.linaro.org/
# 预期 HTTP 200
```

如果不通，先按 3.7 节配置虚拟机代理。

#### 4.3 下载 5 套交叉编译工具链

```bash
cd ~/Radxa_CM5/docker

# 1. gcc-linaro-6.3.1 arm-linux-gnueabihf（99MB）
curl -L -o gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf.tar.xz \
  'https://releases.linaro.org/components/toolchain/binaries/6.3-2017.02/arm-linux-gnueabihf/gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf.tar.xz'

# 2. gcc-linaro-aarch64-none-elf-4.8（50MB）
curl -L -o gcc-linaro-aarch64-none-elf-4.8-2013.11_linux.tar.bz2 \
  'https://releases.linaro.org/archive/13.11/components/toolchain/binaries/gcc-linaro-aarch64-none-elf-4.8-2013.11_linux.tar.bz2'

# 3. gcc-arm-none-eabi-6-2017-q2-update（96MB）
curl -L -o gcc-arm-none-eabi-6-2017-q2-update.tar.bz2 \
  'https://developer.arm.com/-/media/Files/downloads/gnu-rm/6-2017q2/gcc-arm-none-eabi-6-2017-q2-update-linux.tar.bz2'

# 4. gcc-linaro-6.3.1 aarch64-linux-gnu（106MB）
curl -L -o gcc-linaro-6.3.1-2017.02-x86_64_aarch64-linux-gnu.tar.xz \
  'https://releases.linaro.org/components/toolchain/binaries/6.3-2017.02/aarch64-linux-gnu/gcc-linaro-6.3.1-2017.02-x86_64_aarch64-linux-gnu.tar.xz'

# 5. gcc-linaro-7.2.1 aarch64-elf（51MB）
curl -L -o gcc-linaro-7.2.1-2017.11-x86_64_aarch64-elf.tar.xz \
  'https://releases.linaro.org/components/toolchain/binaries/7.2-2017.11/aarch64-elf/gcc-linaro-7.2.1-2017.11-x86_64_aarch64-elf.tar.xz'
```

#### 4.4 解压工具链

```bash
cd ~/Radxa_CM5/docker
tar -xf gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf.tar.xz
tar -xf gcc-linaro-aarch64-none-elf-4.8-2013.11_linux.tar.bz2
tar -xf gcc-arm-none-eabi-6-2017-q2-update.tar.bz2
tar -xf gcc-linaro-6.3.1-2017.02-x86_64_aarch64-linux-gnu.tar.xz
tar -xf gcc-linaro-7.2.1-2017.11-x86_64_aarch64-elf.tar.xz

# 确认解压结果（应有 5 个目录）
ls -d gcc-*/
```

#### 4.5 确认 Dockerfile 包含 COPY 命令

查看 `~/Radxa_CM5/docker/Dockerfile`，确认包含以下 COPY 行：

```dockerfile
COPY ./gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf /opt/gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf
COPY ./gcc-linaro-aarch64-none-elf-4.8-2013.11_linux /opt/gcc-linaro-aarch64-none-elf-4.8-2013.11_linux
COPY ./gcc-linaro-aarch64-none-elf-4.8-2013.11_linux /opt/toolchains/gcc-linaro-aarch64-none-elf-4.8-2013.11_linux
COPY ./gcc-arm-none-eabi-6-2017-q2-update /opt/toolchains/gcc-arm-none-eabi-6-2017-q2-update
COPY ./gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf /opt/toolchains/gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf
COPY ./gcc-linaro-6.3.1-2017.02-x86_64_aarch64-linux-gnu /opt/toolchains/gcc-linaro-6.3.1-2017.02-x86_64_aarch64-linux-gnu
COPY ./gcc-linaro-7.2.1-2017.11-x86_64_aarch64-elf /opt/toolchains/gcc-linaro-7.2.1-2017.11-x86_64_aarch64-elf
```

如果 Dockerfile 中没有这些行（备份时可能是旧版本），用以下完整 Dockerfile 替换：

```bash
cat > ~/Radxa_CM5/docker/Dockerfile << 'EOF'
FROM ubuntu:20.04

RUN rm /etc/apt/sources.list
RUN echo "deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse" | tee /etc/apt/sources.list
RUN echo "deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse" >> /etc/apt/sources.list
RUN echo "deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse" >> /etc/apt/sources.list
RUN echo "deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse" >> /etc/apt/sources.list

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -y && apt-get install -y software-properties-common apt-utils
RUN add-apt-repository -y ppa:deadsnakes/ppa
RUN apt-get update -y && apt-get install -y python3.8
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 150
RUN apt-get install -y python3-pip && pip install pycrypto

RUN apt-get update -y && apt-get install -y openjdk-8-jdk python git-core gnupg flex bison gperf build-essential \
    zip curl gawk liblz4-tool zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 \
    libncurses5 libncurses-dev x11proto-core-dev libx11-dev lib32z-dev ccache \
    libgl1-mesa-dev libxml2-utils xsltproc unzip mtools u-boot-tools \
    htop iotop sysstat iftop pigz bc device-tree-compiler lunzip \
    dosfstools vim-common parted udev libssl-dev sudo rsync python3-pyelftools cpio

RUN curl https://mirrors.tuna.tsinghua.edu.cn/git/git-repo > /usr/local/bin/repo && \
    chmod +x /usr/local/bin/repo && \
    which repo

ENV REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo/'

RUN apt-get install -y lzop swig
RUN apt-get update -y && apt-get install -y tzdata

RUN mkdir /opt/toolchains

COPY ./gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf /opt/gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf
COPY ./gcc-linaro-aarch64-none-elf-4.8-2013.11_linux /opt/gcc-linaro-aarch64-none-elf-4.8-2013.11_linux
COPY ./gcc-linaro-aarch64-none-elf-4.8-2013.11_linux /opt/toolchains/gcc-linaro-aarch64-none-elf-4.8-2013.11_linux
COPY ./gcc-arm-none-eabi-6-2017-q2-update /opt/toolchains/gcc-arm-none-eabi-6-2017-q2-update
COPY ./gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf /opt/toolchains/gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf
COPY ./gcc-linaro-6.3.1-2017.02-x86_64_aarch64-linux-gnu /opt/toolchains/gcc-linaro-6.3.1-2017.02-x86_64_aarch64-linux-gnu
COPY ./gcc-linaro-7.2.1-2017.11-x86_64_aarch64-elf /opt/toolchains/gcc-linaro-7.2.1-2017.11-x86_64_aarch64-elf

RUN apt-get install -y net-tools gcc-arm-linux-gnueabihf gcc-arm-none-eabi

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
EOF
```

#### 4.6 重建 Docker 镜像

```bash
cd ~/Radxa_CM5/docker
sudo docker build -t android-builder:12.x .
# 耗时约 10-15 分钟
```

#### 4.7 验证环境完整

```bash
sudo docker run --rm android-builder:12.x bash -c \
  'echo "=== 工具链 ===" && ls /opt/toolchains/ && \
   echo "" && echo "=== 编译器 ===" && \
   /opt/gcc-linaro-6.3.1-2017.02-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc --version | head -1 && \
   /opt/toolchains/gcc-linaro-7.2.1-2017.11-x86_64_aarch64-elf/bin/aarch64-elf-gcc --version | head -1 && \
   /opt/toolchains/gcc-linaro-6.3.1-2017.02-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc --version | head -1 && \
   java -version 2>&1 | head -1 && python3 --version && which repo'
```

预期输出：
- /opt/toolchains/ 下有 5 个工具链目录
- arm-linux-gnueabihf-gcc 6.3.1
- aarch64-elf-gcc 7.2.1
- aarch64-linux-gnu-gcc 6.3.1
- openjdk 1.8.0
- Python 3.8.x
- /usr/local/bin/repo

#### 4.8 清理下载的压缩包

```bash
cd ~/Radxa_CM5/docker
rm -f *.tar.xz *.tar.bz2
```

注意：Docker 镜像加速器配置（`/etc/docker/daemon.json`）在备份中已保留，但镜像加速地址可能失效。
如果 `docker build` 拉取 ubuntu:20.04 失败，参考 3.4 节更新加速器地址。

## 5.2 新电脑是 Windows — 快速恢复

### 步骤 1：导入虚拟机

1. 新电脑安装 **VMware Workstation 17**
2. VMware → 文件 → 打开 → 选择 `.ovf` 文件
3. 选择存放位置，等待导入
4. 调整参数（右键 → 设置）：内存 16GB+，CPU 8 核+，网络桥接模式

### 步骤 2：恢复网络和代理

1. 启动虚拟机，检查网络
2. 更新 `/etc/profile.d/proxy.sh` 中的 Windows IP（如果 IP 变化）
3. Windows 安装 Clash，开启 Allow LAN，防火墙放行 7890
4. 安装 Tailscale（Windows + 虚拟机），同账号登录

### 步骤 3：恢复 SDK 并对齐 Dockerfile

1. 拷贝 SDK 压缩包到虚拟机 `~/Radxa_CM5/SDK_Android/`
2. 按上方 **步骤 4（4.1~4.8）** 对齐官方 Dockerfile 环境
3. 解压 SDK 并编译验证

### 步骤 4：配置 Mac 远程开发（如有）

按第四部分重新配置 Mac 端 SSH 和 Tailscale。

## 5.3 新电脑是 Mac（远程控制 Windows 开发）

Mac M 芯片无法运行 x86 虚拟机，采用 Mac 远程控制 Windows 方案。

### 前提条件
- Windows 电脑保持运行（作为编译服务器）
- Windows 上已按 5.2 完成虚拟机恢复和 Dockerfile 对齐
- Mac 和 Windows 都安装 Tailscale

### 步骤 1：Windows 端配置

1. 按 5.2 完成虚拟机导入、网络恢复、SDK 恢复、Dockerfile 对齐

2. 安装 Tailscale（Windows 版），用同一账号登录

3. 安装 Clash for Windows，导入订阅，开启 "允许局域网连接入Clash"
   防火墙放行：
   ```powershell
   netsh advfirewall firewall add rule name="Clash Proxy" dir=in action=allow protocol=tcp localport=7890
   ```

4. 启动虚拟机，确认服务正常：
   ```bash
   tailscale status
   sudo systemctl status ssh
   curl -I https://www.google.com    # 验证外网代理
   docker images android-builder     # 验证 Docker 镜像
   ```

5. （可选）设置虚拟机开机自启：VMware → 编辑 → 首选项 → 共享虚拟机 → 开机启动

### 步骤 2：新 Mac 端配置

1. 安装 Tailscale（App Store），用同一账号登录

2. 安装 ClashX Pro，导入订阅

3. 配置 ClashX Pro 与 Tailscale 共存（按第二部分操作）：
   - 添加 Tailscale 直连规则到代理配置
   - 关闭 Tailscale DNS Settings

4. 生成 SSH 密钥并部署：
   ```bash
   ssh-keygen -t ed25519 -C "new-mac-to-linux-vm"
   ssh-copy-id zhangwenqi@<虚拟机Tailscale_IP>
   ```
   注意：Tailscale IP 可能与之前不同，在虚拟机中执行 `tailscale ip -4` 确认。

5. 配置 SSH config（`~/.ssh/config`）：
   ```
   Host linux-vm
       HostName <虚拟机Tailscale_IP>
       User zhangwenqi
       Port 22
       ServerAliveInterval 30
       TCPKeepAlive yes
       ConnectTimeout 60
   ```

6. 安装 VS Code + Remote-SSH 扩展

7. Cmd+Shift+P → "Remote-SSH: Connect to Host" → linux-vm

### 步骤 3：验证

```bash
# Mac 终端测试
ping <虚拟机Tailscale_IP>
ssh linux-vm

# 验证 Docker 环境完整（应显示 5 套工具链）
ssh linux-vm "docker run --rm android-builder:12.x ls /opt/toolchains/"

# 验证 VPN
# 浏览器访问 YouTube
```

### 日常使用流程

1. Windows 电脑开机 → 虚拟机自动启动（或手动启动）
2. Mac 打开 Tailscale + ClashX Pro
3. VS Code → Remote-SSH → linux-vm
4. 打开 `/home/zhangwenqi/Radxa_CM5` 开始开发

---

# 第六部分：故障排除

## 6.1 Tailscale 连不上

```bash
# Linux 端检查
sudo tailscale status
sudo systemctl restart tailscaled
sudo tailscale up

# 诊断
tailscale netcheck
```

## 6.2 SSH 连接被拒绝

```bash
# Linux 端检查
sudo systemctl status ssh
sudo netstat -tlnp | grep :22
sudo ufw status

# 重启 SSH
sudo systemctl restart ssh
```

## 6.3 VPN 开启 Tailscale 后失效

检查两个配置：
1. ClashX Pro 配置文件中 Tailscale 直连规则是否存在（订阅更新会覆盖）
2. Tailscale DNS Settings 是否已关闭

## 6.4 Docker 拉取镜像超时

Docker Hub 被墙，检查镜像加速配置：
```bash
cat /etc/docker/daemon.json
# 确认 registry-mirrors 配置正确
sudo systemctl restart docker
```

## 6.5 VS Code Remote-SSH 连接超时

在 `~/.ssh/config` 中增大超时：
```
Host linux-vm
    ConnectTimeout 120
    ServerAliveInterval 30
    ServerAliveCountMax 5
```

---

# 第七部分：快速参考

## 日常连接命令

```bash
# Mac 终端直连
ssh linux-vm

# 文件传输
scp file.txt linux-vm:~/
scp -r folder/ linux-vm:~/destination/
```

## VS Code 操作

- Cmd+Shift+P → "Remote-SSH: Connect to Host" → linux-vm
- Cmd+Shift+P → "Remote-SSH: Disconnect"

## Linux 虚拟机管理

```bash
# Tailscale
tailscale ip -4
tailscale status
sudo tailscale down / up

# Docker
docker images
docker run -it --name android-build -v ~/Radxa_CM5/SDK_Android:/workspace -w /workspace android-builder:12.x bash
docker start -ai android-build    # 重新进入已创建的容器

# SSH
sudo systemctl status ssh
sudo systemctl restart ssh
```

---

---

# 第八部分：Android SDK Git 代码管理

## 8.1 方案说明

OrangePi CM5 Android 13 SDK 共 84GB、数十万文件，不可能全量入 Git。
采用**白名单 `.gitignore` 方案**：在 SDK 根目录初始化 Git，默认忽略所有文件，只放行会被修改的核心目录。

- Git 仓库路径：`~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13/`
- 远程仓库：GitHub（`OrangePi_CM5_Project`，Private）
- 首次推送（96,602 个文件，.git 约 1.3GB）约需 30-60 分钟

## 8.2 追踪的核心目录

| 目录 | 作用 |
|------|------|
| `device/rockchip/` | 产品配置、开机画面、init.rc、权限 |
| `kernel-5.10/arch/arm64/boot/dts/` | 设备树（屏幕/LED/NFC/GPIO 等） |
| `kernel-5.10/arch/arm64/configs/` | 内核配置文件 |
| `kernel-5.10/drivers/` | 内核驱动源码 |
| `kernel-5.10/include/` | 内核头文件 |
| `hardware/rockchip/` | Rockchip 硬件 HAL |
| `hardware/interfaces/` | Android 硬件接口 |
| `hardware/libhardware/` | Android hardware 库 |
| `u-boot/arch/arm/` | U-Boot 架构相关代码 |
| `u-boot/configs/` | U-Boot 配置 |
| `u-boot/board/` | 板级支持 |
| `u-boot/drivers/` | U-Boot 驱动 |
| `u-boot/include/` | U-Boot 头文件 |
| `u-boot/dts/` | U-Boot 设备树 |
| `frameworks/base/core/res/` | 系统默认资源（开机动画等） |

## 8.3 .gitignore 白名单内容

```gitignore
# 策略：根目录忽略所有，只白名单放行会改的目录

# 1. 默认忽略根目录所有内容
/*

# 2. 白名单：允许这些顶层目录/文件
!/.gitignore
!/README.md
!/device/
!/kernel-5.10/
!/hardware/
!/u-boot/
!/frameworks/

# ===== device 目录：只保留 rockchip =====
/device/*
!/device/rockchip/
/device/rockchip/*
!/device/rockchip/common/
!/device/rockchip/rk3588/

# ===== kernel-5.10：只保留驱动、设备树、配置 =====
/kernel-5.10/*
!/kernel-5.10/arch/
!/kernel-5.10/drivers/
!/kernel-5.10/kernel/
!/kernel-5.10/include/
!/kernel-5.10/Documentation/

# kernel-5.10/arch 只保留 arm64
/kernel-5.10/arch/*
!/kernel-5.10/arch/arm64/
/kernel-5.10/arch/arm64/*
!/kernel-5.10/arch/arm64/boot/
!/kernel-5.10/arch/arm64/configs/
/kernel-5.10/arch/arm64/boot/*
!/kernel-5.10/arch/arm64/boot/dts/

# ===== hardware 只保留 rockchip =====
/hardware/*
!/hardware/rockchip/
!/hardware/interfaces/
!/hardware/libhardware/

# ===== u-boot 只保留配置、设备树、板级代码 =====
/u-boot/*
!/u-boot/arch/
!/u-boot/board/
!/u-boot/configs/
!/u-boot/include/
!/u-boot/drivers/
!/u-boot/dts/
/u-boot/arch/*
!/u-boot/arch/arm/
/u-boot/arch/arm/*
!/u-boot/arch/arm/dts/
!/u-boot/arch/arm/mach-rockchip/
!/u-boot/arch/arm/include/

# ===== frameworks 只保留 base/core/res =====
/frameworks/*
!/frameworks/base/
/frameworks/base/*
!/frameworks/base/core/
/frameworks/base/core/*
!/frameworks/base/core/res/

# ===== 编译产物和临时文件 =====
out/
*.o
*.ko
*.a
*.so
*.so.dbg
*.pyc
*.swp
*.swo
*~
__pycache__/
.DS_Store

# ===== 子仓库的 .git 内部目录 =====
**/.git/
```

## 8.4 日常工作流

```bash
# 在 SDK 中直接改代码（例如 NFC 驱动）
cd ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13
vi kernel-5.10/drivers/nfc/my_nfc.c

# 查看修改
git status
git diff

# 提交
git add kernel-5.10/drivers/nfc/
git commit -m "添加 PN532 NFC 驱动支持"
git push
```

## 8.5 回滚操作

```bash
# 查看历史
git log --oneline

# 回滚单个文件到上一版本
git checkout HEAD~1 -- kernel-5.10/drivers/nfc/my_nfc.c

# 回滚整个目录
git checkout HEAD~1 -- kernel-5.10/drivers/nfc/

# 慎用：回滚全部到某个 commit
git reset --hard <commit_hash>
```

## 8.6 分支管理（推荐功能分支）

```bash
# 创建功能分支
git checkout -b feature/nfc-pn532

# 开发、多次 commit 后合并回 main
git checkout main
git merge feature/nfc-pn532

# 废弃分支
git branch -D feature/nfc-pn532
```

## 8.7 首次推送到 GitHub（SSH 密钥方式，推荐）

**当前仓库**: `git@github.com:Leo-z8/OrangePi_CM5_Project.git`（Private）
**认证方式**: SSH ed25519 密钥，永久免密
**网络路径**: 虚拟机 → GitHub `ssh.github.com:443`（直连，不经 Clash 代理）

### 为什么选 SSH 而非 HTTPS + PAT

| 对比项 | HTTPS + PAT | SSH 密钥 |
|--------|-------------|----------|
| 有效期 | 最长 1 年，需定期更新 | 永久有效 |
| 网络路径 | 必须走 Clash 代理（HTTPS 国内慢） | 443 端口直连 GitHub |
| 稳定性 | TLS 握手依赖代理链，大文件易挂断 | SSH 原生流控，错误信息清晰 |
| 推送速度 | 受 Clash 带宽 + rwnd 限制 | 实测稳定 4-6 MiB/s |
| 首次推送 758 MiB | 多次中断重试 | 约 2-3 分钟完成 |

### 步骤 1：虚拟机生成 SSH 密钥

```bash
ssh-keygen -t ed25519 -C "zhangwenqi-orangepi-vm-github" \
    -f ~/.ssh/github_ed25519 -N ''
```

生成后两个文件：
- `~/.ssh/github_ed25519`（私钥，权限 600）
- `~/.ssh/github_ed25519.pub`（公钥）

### 步骤 2：配置 `~/.ssh/config` 走 443 端口

```
Host github.com
    HostName ssh.github.com
    User git
    Port 443
    IdentityFile ~/.ssh/github_ed25519
    IdentitiesOnly yes
    ServerAliveInterval 30
```

**关键点**:
- `HostName ssh.github.com` + `Port 443` 绕过国内对 22 端口的封锁
- `IdentitiesOnly yes` 强制只用这个密钥，防止扔其他密钥被 GitHub 拒绝
- 此方案不依赖 Clash 代理，虚拟机可直连

### 步骤 3：添加公钥到 GitHub

```bash
cat ~/.ssh/github_ed25519.pub
```

复制整行内容，到 https://github.com/settings/ssh/new 添加：
- Title：`OrangePi-VM-Linux`
- Key type：Authentication Key
- Key：粘贴公钥

### 步骤 4：测试连通性

```bash
ssh -T git@github.com
# 预期输出：Hi Leo-z8! You've successfully authenticated
```

### 步骤 5：切换仓库 remote 到 SSH

```bash
cd ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13

# 清除旧 HTTPS 配置（如果之前用过 PAT）
rm -f ~/.git-credentials
git config --local --unset credential.helper 2>/dev/null
git config --local --unset http.proxy 2>/dev/null
git config --local --unset https.proxy 2>/dev/null

# 改 remote 为 SSH
git remote set-url origin git@github.com:Leo-z8/OrangePi_CM5_Project.git
git remote -v

# 推送（永久免密）
git push -u origin main
```

### 备选方案：HTTPS + PAT（不推荐作为长期方案）

仅在没有 SSH 配置权限或临时应急时使用：

```bash
# 配置 git 代理（HTTPS 必须走 Clash）
git config http.proxy http://192.168.6.57:7890
git config https.proxy http://192.168.6.57:7890
git config http.postBuffer 524288000   # 增大缓冲，大仓库必需

git remote set-url origin https://github.com/Leo-z8/OrangePi_CM5_Project.git
git config credential.helper store

# 提示时：用户名 = GitHub 账号，密码 = PAT 令牌
git push -u origin main
```

> **PAT 生成**：https://github.com/settings/tokens/new → 勾 `repo` → 生成后立即复制。
> 推送完成后建议立即 Revoke（一次性使用），因为 PAT 明文存在 `~/.git-credentials` 有泄露风险。

## 8.8 推送后日常 push

SSH 方式配置后完全免密：

```bash
cd ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13

git add kernel-5.10/drivers/nfc/
git commit -m "添加 PN532 NFC 驱动"
git push       # 免密，永久免代理
git pull       # 同理
```

## 8.9 备份策略

| 资产 | 备份位置 |
|------|---------|
| 驱动/配置修改 | GitHub 仓库（`Leo-z8/OrangePi_CM5_Project`，Private） |
| SDK 原始源码分卷（9个 tar.gz） | Windows 电脑（永久保留，不能删） |
| Docker 镜像定义 | `~/OrangePi_CM5/docker/Dockerfile` |
| 编译产物（update.img） | 按需保留，体积大不进 Git |
| 被 `.gitignore` 排除的大文件 | 需单独备份（见 8.10） |

## 8.10 GitHub 推送踩坑实录（2026-04-22 首次推送）

### 踩坑 1：GitHub 单文件 100MB 硬性限制

**现象**：HTTPS 推送数据上传完后 "远端意外挂断了"；SSH 推送明确报错：

```
remote: error: File device/rockchip/rk3588/rk3588m_car/preinstall/camera360.apk
is 120.48 MB; this exceeds GitHub's file size limit of 100.00 MB
remote: error: GH001: Large files detected.
! [remote rejected]     main -> main (pre-receive hook declined)
```

**根因**：GitHub pre-receive hook 在服务端验收阶段拒绝 >100MB 单文件。HTTPS 协议表现为 TLS 连接异常终止（难诊断），SSH 协议会直接打印错误（易诊断）。

**教训**：**大文件推送问题要用 SSH 调试**，HTTPS 的"远端挂断"往往掩盖真实原因。

### 踩坑 2：扫大文件要用 `git ls-tree`，不能用 `git rev-list`

```bash
# ❌ 错误方式：按 blob 扫描，Git 去重会掩盖多路径
git rev-list --objects --all | \
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
  awk '$1=="blob" && $3>=100000000'

# ✅ 正确方式：按 HEAD 中的路径扫描
git ls-tree -r -l HEAD | awk '$4>=100000000 {printf "%.1f MB  %s\n", $4/1024/1024, $5}'
```

**根因**：两个产品目录下存在完全相同的 `camera360.apk`（内容 hash 相同），Git 按 blob 去重存储，`rev-list` 只列出一个 blob，但 `ls-tree` 会展开所有路径。

**教训**：初次 `git rm --cached` 只去掉一个路径，第二次推送时另一个路径的 APK 仍会撞限，必须一次性用通配符覆盖所有。

### 踩坑 3：`.gitignore` 用具体路径易遗漏，通配符更健壮

```gitignore
# ❌ 窄规则：只管一个路径
device/rockchip/rk3588/rk3588m_car/preinstall/camera360.apk

# ✅ 通配符：所有产品配置下的预装 APK 一次性处理
device/rockchip/*/*/preinstall/*.apk
```

**教训**：多产品配置项目中（rk3588m_car / rk3588m_s / …）类似的二进制资源可能分布在多个目录，写 `.gitignore` 规则时默认用通配符。

### 踩坑 4：HTTPS + Clash 代理的 TCP rwnd 瓶颈

推送过程 `ss -tni` 显示 **`rwnd_limited: 95.1%`** —— 95% 时间卡在 GitHub 端接收窗口，Clash 代理到 GitHub 的上行链路成瓶颈，平均速度被压到 1-2 MiB/s。

切到 SSH 443 直连后 **`rwnd_limited: 0.8%`**，速度稳定 4-6 MiB/s。

**结论**：大仓库首次推送坚决用 SSH，不要 HTTPS。

### 踩坑 5：凭证泄露风险

HTTPS + PAT 方案下，凭证明文存在 `~/.git-credentials`：

```
https://用户名:PAT@github.com
```

即使 PAT 只用一次，也要：
1. 推送完成后到 https://github.com/settings/tokens 立即 Revoke
2. `rm -f ~/.git-credentials`
3. `git config --unset credential.helper`

**SSH 方式无此风险**：私钥只存本地 ~/.ssh/，不会经过对话或日志。

### 本次推送完整大文件排除流程（已落地）

```bash
cd ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13

# 1. 扫出所有 >100MB 路径
git ls-tree -r -l HEAD | awk '$4>=100000000 {printf "%.1f MB  %s\n", $4/1024/1024, $5}'

# 2. 从索引移除（磁盘文件保留）
git rm --cached device/rockchip/rk3588/rk3588m_car/preinstall/camera360.apk
git rm --cached device/rockchip/rk3588/rk3588m_s/preinstall/camera360.apk

# 3. .gitignore 追加通配符规则
cat >> .gitignore << 'EOF'

# 排除所有预装 APK（GitHub 100MB 限制 + 非驱动开发必需）
device/rockchip/*/*/preinstall/*.apk
EOF

# 4. amend 原 commit
git add .gitignore
git commit --amend --no-edit

# 5. 验证 HEAD 里无 >100MB 文件
git ls-tree -r -l HEAD | awk '$4>=100000000'   # 应无输出

# 6. 推送
git push -u origin main
```

### 需要单独备份的大文件清单

以下文件已从 Git 历史排除，需另存（网盘/内网文件服务器）：

| 文件 | 大小 | 路径 |
|------|------|------|
| camera360.apk (rk3588m_car) | 120 MB | `device/rockchip/rk3588/rk3588m_car/preinstall/` |
| camera360.apk (rk3588m_s) | 120 MB | `device/rockchip/rk3588/rk3588m_s/preinstall/` |

这些是产品预装 APK，不是驱动开发所需，完整 SDK 部署时再从备份恢复即可。

## 8.11 GitHub 推送关键数据（本次首次推送）

| 指标 | 数值 |
|------|------|
| 本地仓库大小（`.git`） | 774 MB |
| 追踪文件总数 | 96,600 |
| 上传 pack 大小 | 约 518 MiB（移除两个 120MB APK 后） |
| 网络路径 | 虚拟机 → Tailscale（不经 Clash） → `ssh.github.com:443` |
| 实测速度 | 4-6 MiB/s（SSH 443 直连，无 rwnd 瓶颈） |
| 首次推送耗时 | 约 2-3 分钟（纯网络时间，排除多次重推） |
| 最终 commit | `c79995a30a9db560899bf0845440720edf3e5e3d` |
| 远程仓库 | https://github.com/Leo-z8/OrangePi_CM5_Project |

---

# 第九部分：Mac 本地 + 虚拟机编译 的双端开发工作流（2026-04-23 落地，推荐）

## 9.1 为什么采用这种方案

### 背景：虚拟机内用 Claude CLI 不可行

实测发现（2026-04-23）：
- **Anthropic 对中国机场 IP 做了 API 级风控**，`api.anthropic.com` 对机场 IP 返回 ECONNRESET 或静默丢包（但 HTTPS 握手能成功、`console.anthropic.com` 也能通，仅 `/v1/messages` 被精准拉黑）
- 尝试过多种节点（美国 IEPL / 香港 / 新加坡），机场共享 IP 段都被 Ban
- 虚拟机内装 Claude CLI（Node 20 + `@anthropic-ai/claude-code` 2.1.118）后，启动永远卡在 "Checking connectivity..." 或 ECONNRESET
- 新版 Claude CLI（2.1.118+）打包成独立二进制，屏蔽 `NODE_OPTIONS` 等代理注入手段

### 解决方向

**Mac 能连 Anthropic（你已在用），Linux 虚拟机保留编译能力。让两边各司其职**：

```
Mac                                    GitHub                Linux 虚拟机
Claude Code + 代码仓库    ──push──▶   dev/mac    ◀──pull──    完整 SDK + 编译工具链
(读写/改/grep/Edit)                  分支                    (./make.sh 编译)
       ▲                                                           │
       └─────────── Claude 通过 SSH 直接操作虚拟机编译 ────────────┘
```

Claude 可以**通过 Bash 工具 SSH 到虚拟机自动编译**，实现全自动闭环，**不需要用户在两端手动搬运错误信息**。

## 9.2 Mac 端一次性环境配置

### 9.2.1 创建 case-sensitive APFS 卷（关键，否则会踩大坑）

Linux 内核源码含**同名但大小写不同的文件**（如 `xt_CONNMARK.h` / `xt_connmark.h`），macOS 默认 APFS 是 case-insensitive，会把这些文件合并成一个，导致 `git status` 永远脏且无法还原。

**解决**：建一个专门的 case-sensitive 卷放 Linux 项目。

1. 打开 **磁盘工具**
2. 左侧选 **Container disk3**（Macintosh HD 的父容器）
3. 菜单「编辑 → 添加 APFS 卷...」
4. 弹窗中：
   - 名称：`LinuxDev`
   - **格式：APFS（区分大小写）** ← 关键
   - 大小：不限制（APFS 容器自动共享空间）
5. 确认 / 添加

验证成功：
```bash
diskutil info /Volumes/LinuxDev | grep Personality
# 应输出: File System Personality:   Case-sensitive APFS

# 实验创建大小写同名文件
touch /Volumes/LinuxDev/_test_a /Volumes/LinuxDev/_test_A
ls /Volumes/LinuxDev/_test_*
# 应输出两行（_test_A 和 _test_a）
rm /Volumes/LinuxDev/_test_*
```

从此 Linux 项目都放 `/Volumes/LinuxDev/` 下。

### 9.2.2 Mac 端 Git 配置

```bash
# 只需要配一次，以后所有项目都复用
git config --global user.name  "你的名字"
git config --global user.email "你的邮箱"
git config --global credential.helper osxkeychain   # PAT 存钥匙串
```

### 9.2.3 首次克隆到 case-sensitive 卷

```bash
cd /Volumes/LinuxDev
git clone https://github.com/Leo-z8/OrangePi_CM5_Project.git
cd OrangePi_CM5_Project
# 首次弹出凭证提示：
#   Username: Leo-z8
#   Password: 粘贴 PAT (ghp_xxx...)
# 之后 PAT 存钥匙串永久免密
```

> **重要：不要用 GitHub Web 下载 ZIP**（文件夹带 `-main` 后缀那种）
> ZIP 不含 `.git/` 元数据，无法 push。必须 `git clone`。

### 9.2.4 创建开发分支

```bash
cd /Volumes/LinuxDev/OrangePi_CM5_Project
git checkout -b dev/mac
git push -u origin dev/mac
```

## 9.3 虚拟机端一次性环境配置

如果之前已按本文档 8.7 配好 SSH ed25519 over 443，这步跳过。否则见 8.7。

确认：
```bash
ssh linux-vm "cd ~/OrangePi_CM5/Aandroid_OrangePi/merged/Android_13 && git remote -v"
# 应该显示 origin  git@github.com:Leo-z8/OrangePi_CM5_Project.git
```

## 9.4 日常开发闭环（Claude 自主执行）

Claude 收到开发任务后会自动执行：

```
┌──────────────────────────────────────────────────────────┐
│ Claude 在 Mac 端工作                                      │
│  1. cd /Volumes/LinuxDev/OrangePi_CM5_Project            │
│  2. 读源码 / grep / 分析                                  │
│  3. Edit 修改代码                                         │
│  4. git add xxx                                           │
│  5. git commit -m "描述"                                  │
│  6. git push origin dev/mac                               │
└──────────────────┬───────────────────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────────────────┐
│ Claude 通过 Bash 工具 SSH 到虚拟机                        │
│  ssh linux-vm 'cd ~/OrangePi_CM5/.../Android_13 && \      │
│    git fetch origin && \                                  │
│    git checkout dev/mac && git pull && \                  │
│    ./make.sh -F -b orangepicm5-tablet'                    │
│                                                           │
│  编译输出直接进 Claude 的上下文                           │
└──────────────────┬───────────────────────────────────────┘
                   │
                   ▼
         ┌─────────────────────┐
         │  编译成功？          │
         └──────┬──────────────┘
                │ 失败            │ 成功
                ▼                 ▼
        Claude 读错误          通知用户烧录
        分析 → 修代码           (物理操作)
        再 push 再编
        （自循环）
```

### 用户只在三个场景介入
1. 提需求（"给 NFC 加 PN532 驱动"）
2. 烧录到开发板（USB 接线、按 Loader 键）
3. 判断结果（"这效果对不对"）

## 9.5 分支策略

```
main                              ← 保护分支，只接受编译+烧录测试通过的代码
  ├─ dev/mac                       ← Mac 端 Claude 日常开发推这里
  ├─ feature/lcd-st77916-mipi     ← 可选：大功能单独拉 feature 分支
  └─ feature/nfc-pn532
```

**合并原则**：
- dev/mac 的改动在虚拟机编译通过 + 烧录验证 OK 后，才合并回 main
- 合并命令（在虚拟机端或 Mac 端都行）：
  ```bash
  git checkout main
  git merge dev/mac --no-ff -m "集成 NFC PN532 驱动"
  git push origin main
  ```

## 9.6 项目级 CLAUDE.md（关键）

在仓库根目录放一个 `CLAUDE.md`，**新的 Claude 对话打开项目时自动加载**，无需用户再解释工作流。

CLAUDE.md 内容要点（完整版见仓库）：
- 项目本质（硬件、内核版本、开发类型）
- 双端架构（Mac 路径 + VM 路径）
- 关键命令（git push、SSH、编译）
- 全自动闭环流程
- 只可修改的目录清单
- 环境约束（case-sensitive 卷、不能 >100MB 文件等）
- 踩坑速查
- 用户偏好（中文、代码风格、响应风格）

## 9.7 本方案踩过的大坑总结

### 坑 1：ClashX 增强模式对 SSH 协议干扰

Mac 上 ClashX Pro 的 TUN 模式把 `ssh.github.com:443` 劫持到 fake-ip（`198.18.x.x`），但 **SSH 协议在 banner exchange 阶段被切**（`Connection timed out during banner exchange`），而 HTTPS 到同一域名却能通。

原因推测：某些节点对非 HTTP/HTTPS 流量做 DPI 过滤。

**规避**：Mac 端 Git 操作**只用 HTTPS + PAT**，不用 SSH。
虚拟机端没这问题（Tailscale 直连，没经过 Mac ClashX）。

### 坑 2：ClashX 的 7890 端口虽然开着但实际不转发

Mac ClashX Pro 当前配置下：
- ✅ TUN 透明代理工作（fake-ip 路径）
- ❌ 7890 HTTP 代理端口虽然能 `nc -z` 成功但不实际转发（curl -x 超时）
- ❌ 7891 SOCKS5 没开

意味着所有"显式指定代理"的方案（`git config http.proxy`、SSH ProxyCommand、proxychains）都不能用。

**规避**：依赖 TUN 透明路径，不用显式代理。

### 坑 3：default APFS 与 Linux 内核大小写冲突

macOS 主盘默认 APFS 是 case-insensitive，Linux 内核源码（netfilter 等）有大小写同名文件，checkout 时后者覆盖前者，`git status` 永远显示 8 个 .h 文件 modified 且不可还原。

**规避**：case-sensitive APFS 卷。见 9.2.1。

### 坑 4：GitHub ZIP 下载不含 .git

用户若因网络问题改用 GitHub Web 下载 ZIP（带 `-main` 后缀），后续无法 git push。必须 `git clone` 才有 `.git/`。

### 坑 5：SMB 挂载 Mac Finder 污染 `._*` 和 `.DS_Store`

Mac Finder 访问 SMB 共享时会写入 AppleDouble 元数据。
**规避**：`.gitignore` 已加规则 `._*` + `.DS_Store`。

### 坑 6：虚拟机 mihomo TUN 无法访问 Anthropic

即使在虚拟机内装 mihomo 跑 TUN 模式（透明代理），Anthropic 对机场 IP 的 API 级风控仍生效（TLS 握手成功但 POST 请求静默被丢）。

**规避**：根本不在虚拟机用 Claude。用本节方案。

## 9.8 性能参考（2026-04-23 实测）

| 指标 | 数值 |
|------|------|
| 仓库 clone 大小 | ~640 MB（.git） + ~2.3 GB（work tree）= 2.9 GB |
| Mac 端 clone 速度 | 17.5 MiB/s（走 ClashX TUN，GitHub HTTPS）|
| Mac → GitHub push 延迟 | 秒级（小改动）|
| 虚拟机 git pull 速度 | MB/s 级（Tailscale + SSH ed25519 over 443 直连）|
| VS Code 打开 case-sensitive 卷项目 | 索引 < 30 秒（本地速度）|

---

文档版本: 3.2
最后更新: 2026-04-23
适用系统: Ubuntu 20.04/22.04 LTS (x86_64), macOS
网络环境: 跨网络远程开发（Tailscale + ClashX Pro 共存 + GitHub SSH 443）
主要变更（v3.2）:
- **新增第九部分：Mac 本地 + 虚拟机编译 的双端开发工作流**（推荐，规避 Anthropic 对机场 IP 的风控）
- 9.1 分析为什么不能在虚拟机内用 Claude
- 9.2 Mac 端环境配置：**case-sensitive APFS 卷**（避免 Linux 内核大小写冲突）+ HTTPS+PAT+Keychain 免密
- 9.3 虚拟机端 git SSH 配置（延续 v3.1 的 8.7）
- 9.4 Claude 自动化开发闭环流程（Mac 改 → push → SSH 虚拟机编译 → 看错误自修 → 循环）
- 9.5 分支策略（main / dev/mac / feature/*）
- 9.6 项目级 CLAUDE.md 机制（新对话零摩擦接手）
- 9.7 本方案全部踩坑：ClashX SSH 劫持、7890 代理端口不转发、APFS 大小写、mihomo 绕不过 Anthropic 风控等
- 9.8 实测性能数据

主要变更（v3.1）:
- 4.5 新增 SMB 挂载方式（Mac 端轻量文件访问方案）与 Remote-SSH 对比
- 8.7 首次推送 GitHub 改为 SSH 密钥为主，PAT 降为备选
- 8.10 新增首次推送踩坑实录（单文件 100MB 限制、Git 去重陷阱、rwnd 瓶颈等）
- 8.11 新增首次推送实测数据
