这章解释一件具体的事:为什么这台 Ubuntu 既能运行 Pysolate,也能在修改 Guest 后,自己生成新的 Wasm,最后在断网环境中完成演示。
先读 工程工具与数据 和 程序怎样运行。不需要先学完整 Linux 运维。具体安装命令与版本以 构建与来源 为准,这里重点解释原理和判断方法。
1. Windows、WSL、Ubuntu 和 SSH 分别是什么
Windows 是电脑上的操作系统。WSL2 让 Windows 通过轻量虚拟机运行 Linux 内核。内核负责进程、内存、文件和网络等基础资源。Ubuntu 是 Linux 发行版:除了内核,还有命令行工具、软件包管理器和系统配置。
所以,WSL2 中的 Ubuntu 不只是一个长得像 Linux 的终端。它能运行 Linux 程序,也能使用内核提供的 network namespace。它和普通 Ubuntu 服务器仍有差别,例如网络连接经过 Windows 环境,部分功能取决于 WSL 的内核和配置。
SSH 是通过网络连接另一台机器、在那边执行命令的协议。你在 Mac 的终端输入 SSH 命令后,命令可能实际运行在 Ubuntu 上。窗口在哪里,不等于程序在哪里运行。
在当前终端执行:
uname -sm
pwd
第一条显示内核名称和 CPU 架构;这台构建环境应为 Linux x86_64。第二条显示当前工作目录。它们能帮助判断“我现在在哪台系统、哪个目录里”,但不能单独证明服务器身份。
SSH 还有两种容易混淆的密钥关系:
- 服务器主机密钥让客户端确认“我连接的是不是预期服务器”。首次连接通常需要通过可信渠道核对指纹。指纹是公钥的短摘要,不是密码。
- 用户登录密钥让服务器确认“这个连接者能不能登录这个账号”。私钥留在持有者的机器上,不应复制给别人或贴进聊天。
因此,Host key verification failed 和 Permission denied (publickey) 是不同层的问题。前者不要靠关闭身份校验解决,后者也不代表服务器离线。
2. 一个项目里,可能有两条构建路线
回顾 执行层次:用户写的是 Python,但运行它的是编译进 Wasm 的 CPython,Wasm 又由 Go 程序中的 wazero 执行。
下面是结构示意,不是终端输出:
Go 源码 ──Go 编译器──→ Ubuntu 上运行的 Host 程序
CPython 的 C 源码 ──WASI 工具链──→ CPython 静态库
│
guest/runtime.c ────────────────────────┤ 链接
↓
未打包的 Wasm
│
Python 标准库、bootstrap、PLM、prefix ───┤ 打包
↓
dist/spine.wasm
编译把源码变成目标代码。链接把目标代码及它依赖的库连接起来,解决“这个函数在哪里实现”的问题。打包把运行还需要的文件放进可交付产物中。
在本项目中,Go 编译不会顺便把新的 guest/plm.py 放进已有 Wasm。两条路线必须分别处理。
3. 什么是交叉编译
Ubuntu 上的 CPU 能执行 x86_64 机器指令,但 Wasm 不是 x86_64 指令。构建程序运行的平台和它生成代码的平台不同,这叫交叉编译。
这里:
- 编译器本身是 Linux x86_64 程序,运行在 WSL 中。
- 它的目标是
wasm32-wasip1:32 位 Wasm 地址模型,加上 WASI Preview 1 的系统接口约定。 - 生成的文件交给 Wasm runtime 执行,而不是直接当作 Linux 程序启动。
注意“Host”在不同上下文中可能指不同角色。交叉编译讨论的是工具运行与代码目标平台;Pysolate 的 Host/Guest 讨论的是 Go 权限边界和 Wasm 执行边界。不能只看到同一个词就认为它们指同一层。
CPython 的构建还需要一个能在构建机器上执行的 Python 来生成部分构建文件。因此准备流程会构建本机版本和 WASI 目标版本。出现两套构建目录不等于误装了两个演示 runtime。
4. SDK、头文件、静态库和 sysroot
SDK 是开发工具集合。这里的 WASI SDK 包括编译器、链接工具和目标环境需要的文件。
C 程序里常见:
#include <Python.h>
这是代码片段,不是完整程序。头文件告诉编译器有哪些类型、函数声明和常量。只知道 Py_Initialize 的声明,还没有它的实现。
静态库通常以 .a 结尾,里面存放已经编译好的目标文件。链接器从中取得需要的实现。libpython3.14.a 是 CPython 的主体,项目还会链接其他支持库。
sysroot 是交叉编译时使用的目标头文件与库目录。目标是 WASI,就不能随手拿 Ubuntu 自己的系统库来替代。它们的指令格式、调用约定和可用系统接口可能不同。
可以把构建失败分成几个位置:
- 找不到
Python.h:先看头文件位置和编译参数。 undefined symbol:先看链接时是否提供了实现,以及库版本是否匹配。- Wasm validator 拒绝文件:目标产物没有通过格式或特性检查。
- Wasm 能加载但程序报错:已经进入执行层,不再只是“有没有安装编译器”的问题。
这些是排查方向,不是看到关键词就能确定唯一原因。
5. 为什么首次准备慢,之后改 Guest 不必从头来
make setup-build 下载锁定版本的输入,检查下载内容的 SHA-256,再编译 CPython。
版本锁说明这次使用哪个版本。SHA-256把文件内容转换成固定长度的摘要,用于检查下载文件是否和预期字节一致。它不能单独证明代码没有漏洞,预期摘要及其来源本身也必须可信。
缓存是保留已经下载或生成的文件,以便下次复用。缓存有用的前提是输入仍然适用,不能因为文件存在就认定它对应最新源码。
本项目把两种工作分开:
make setup-build:准备较少变化的编译工具、CPython 头文件和静态库。make guest:用这些输入链接当前 C bridge,打包当前 Guest Python 文件。
读 准备脚本 时,按“平台检查 → 下载与校验 → 解压 → CPython 构建”阅读。再读 Guest 构建脚本,找到链接、打包、验证和替换 dist/spine.wasm 的位置。
脚本先在临时目录生成并验证 Wasm,成功后才替换正式文件。失败后旧产物可能仍在,这是保护已有文件,不代表刚才的源码修改已经生效。构建失败时不要拿旧文件继续演示新功能。
6. 修改不同文件,要重建哪一层
先自己回答,再对照下面的规则:
- 改
examples/order.py:这是运行时读取的用户程序,直接重新运行。 - 改 Go 工具实现:重新运行
go run会构建需要更新的 Go 代码,无需重建 Guest。 - 改
guest/plm.py、guest/prefix.py或guest/bootstrap.py:必须make guest,因为这些文件被打包进 Wasm。 - 改
guest/runtime.c:必须make guest,因为它需要重新编译、链接。 - 改站点讲义:
make site更新静态页面,make read只提供已生成的页面,不负责重新构建。
小实验:区分输入程序与打包代码
先运行:
make explain
查看实际打印的 PLM 转换和结果。再运行同一个程序,只改输入:
go run ./cmd/demo -source examples/order.py -inputs '{"item":"pen","quantity":3}' -plm
这条命令已在真实 Wasm 上验证,输出:
{"item": "pen", "total": 14}
价格为 3,数量为 3,运费为 5,所以合计是 14。输入是在运行时传入的,不需要重建 Wasm。接着可以在自己的练习副本中修改用户程序的结果表达式,再用 -source 指向该文件运行,同样不用重建 Guest。
再思考:如果修改的是 AST pass 本身,为什么同样的方法不够?答案不是“Python 比较特殊”,而是 Guest pass 的副本在打包产物里,用户程序则是运行时传入的输入。
真实 demo 的使用方法见 边界例子。不要为了做这个实验直接改坏正在准备演示的文件。
7. 离线:不下载和不能联网是两回事
设置 GOPROXY=off 会告诉 Go 不要从模块代理下载依赖,但它不是整台进程的网络防火墙。其他代码仍可能直接打开网络连接。
因此,离线验收要同时回答:
- 所需依赖是否已经在本机?
- 本次执行是否真的在无法访问网络的条件下成功?
Linux 的 **namespace(命名空间)**让进程看到一组相对独立的系统资源。这里不是 Python 的变量命名空间。
network namespace 隔离网络设备、路由等资源。新建的 namespace 不会自动继承原来的外部网络连接。路由告诉内核怎样把数据包送到目的地址;没有可用路径时,连接会失败。
user namespace 提供独立的用户 ID 和权限视图,让普通用户在系统允许时创建这些隔离环境。
本项目使用:
unshare --user --map-root-user --net
这行是在展示选项,实际命令后面还要接需要运行的程序:
unshare:让新进程使用新的 namespace。--user:创建 user namespace。--map-root-user:在新 namespace 中将当前用户映射为 root。它不意味着获得宿主系统的无限管理员权限。--net:创建 network namespace。
这不是完整容器,也不是通用恶意代码沙箱。它没有因此隔离所有文件、所有进程信息或其他通信途径。本项目用它检查开发命令是否依赖外部网络,不把它当成新的 Guest 安全边界。
8. 怎样实际证明网络被阻断
在已准备好的 Ubuntu 中运行这个小实验:
bash tools/offline-check.sh --probe-only
该命令只运行隔离探针,不跑全套测试。本项目在 WSL 的实际结果是:
Verified: child network is unreachable (errno 101: Network is unreachable)
Offline isolation probe passed.
探针尝试连接一个直接给出的 IP 地址,避免把 DNS 解析失败误当作网络隔离证据。DNS负责把域名转换成地址;这里不需要它。
errno 是系统调用错误的编号。Linux 的 ENETUNREACH 表示网络不可达。脚本不把任意异常都算作成功:连接超时、Python 报错或工具缺失,都不是“隔离已经生效”的充分证据。
完整的 make offline 在同一个隔离子进程环境中先检查网络,再运行测试、站点构建和 demo。不能先在隔离环境中探测,随后回到普通环境测试,却声称测试也断网了。
原来的 SSH 连接和其他程序不会因为这个子进程退出而被断网。如果系统禁止普通用户创建 namespace,命令应明确失败;不要自动退回联网测试。
9. 为什么测试要分层
改一行文档就跑全部真实 Wasm 测试,没有必要;只通过一个小单测就宣称整个 demo 可用,也不够。
本项目有三种检查,各自回答不同问题:
- Python AST 单测:转换逻辑是否符合预期?它不证明新代码已经进入 Wasm。
- 真实 Wasm 测试:打包后的 CPython 是否能通过 Host bridge 正确执行?
- 离线验收:依赖、artifact、测试、文档构建和 demo 能否一起在禁网条件下工作?
日常优先跑与修改相关的检查。阶段完成或正式演示前再跑 make offline,其中已经包含 make check,不用紧接着重复一遍全套。
准备脚本下载依赖时需要联网,不要把它与“依赖齐备后的离线重建”混为一谈。新的空机器不能凭空获得编译工具和库。
10. 排错时先确定失败层
按这个顺序问:
- 连得上机器吗? SSH 身份校验、用户认证和命令执行是三个不同阶段。
- 命令在哪执行? 检查系统和当前目录,避免在 Mac 上运行 Linux 构建流程。
- 缺工具还是缺产物?
command not found和缺少dist/spine.wasm不是同一种问题。 - 刚才的构建成功了吗? 查看退出状态和错误,不只看见一个旧文件就继续。
- 实际用了哪个 Wasm? CLI 的
-wasm可以指定另一份 artifact,检查自己有没有运行错文件。 - 是源码测试失败,还是实际 Guest 失败? 两者对应的修改位置可能不同。
- 是否仅在禁网环境失败? 检查遗漏的依赖,不先解除隔离来让验收变绿。
自测题
- 为什么 Go 代码编译成功,不能证明新的 PLM pass 已经进入 Wasm?
- 为什么有 CPython 的头文件,还可能链接失败?
- 为什么 namespace 里显示 root,不等于拥有宿主 root 权限?
- 为什么下载缓存齐备,仍需要一次真实离线验收?
- 为什么
make read成功启动,也可能展示旧文档?
能用“输入在哪里、谁读取它、哪一步生成产物、失败在哪一层”回答这些问题,就能在现场修改需求时判断该重建什么,而不是靠反复执行所有命令碰运气。