用 Wasmer 运行 Python 并安装依赖
什么是 Wasmer
Wasmer 是一个 WebAssembly 运行时,可以把各种语言编译成 .wasm 然后在任何地方运行。它社区维护了一个 Python 的 WASIX 构建,让你可以在不安装 Python 的机器上跑 Python 脚本:
wasmer run python/python@3.13 -- -c "print('hello world')"
一行命令,不需要安装 Python,不需要管版本。这在 CI、临时脚本、或者不想污染本地环境时特别有用。
问题:怎么装依赖
直接用 pip install 是不行的——WASIX 构建的 Python 缺少 mmap 等 C 模块,pip 的 install 命令跑不起来。
但换个思路:不用 pip,直接用 Python 标准库下载 wheel 解压到 site-packages。Python 的 wheel 本质上就是个 zip 包,解压到 site-packages 目录就能 import。
两类依赖
Python 包分两种:
- 纯 Python 包(如
pytz、six):wheel 里只有.py文件,任何平台都能用,从 PyPI 下载即可 - 含 C 扩展的包(如
numpy、pandas):wheel 里有编译好的.so/.dll,需要对应平台的预编译版本
对于第二类,Wasmer 社区维护了一个 WASIX Python 索引,提供了 60+ 常用包的 WASIX 预编译 wheel。
完整方案
下面这个脚本可以在 Wasmer Python 里直接运行,自动处理两类依赖:
import sys, os, zipfile, urllib.request, re, tempfile, json
site_packages = os.path.join(
sys.base_exec_prefix, 'lib',
f'python{sys.version_info.major}.{sys.version_info.minor}',
'site-packages'
)
os.makedirs(site_packages, exist_ok=True)
def install(name):
# 先试 WASIX 索引(含 C 扩展的包)
try:
url = f'https://pythonindex.wasix.org/simple/{name}/'
html = urllib.request.urlopen(url, timeout=30).read().decode()
links = re.findall(
r'href="([^"]+cp313-cp313-wasix_wasm32\.whl[^"]*)"', html
)
if links:
target = ('https://pythonindex.wasix.org/packages/'
+ links[0].split('/')[-1].split('#')[0])
print(f'[wasix] {name}...')
resp = urllib.request.urlopen(target, timeout=120)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.whl')
tmp.write(resp.read()); tmp.close()
with zipfile.ZipFile(tmp.name) as zf:
zf.extractall(site_packages)
os.unlink(tmp.name)
return True
except:
pass
# 再从 PyPI 下载纯 Python wheel
data = json.loads(
urllib.request.urlopen(f'https://pypi.org/pypi/{name}/json').read()
)
ver = data['info']['version']
for f in data['releases'].get(ver, []):
fn = f['filename']
if 'py3-none-any' in fn and fn.endswith('.whl'):
print(f'[pure] {name} {ver}...')
resp = urllib.request.urlopen(f['url'], timeout=60)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.whl')
tmp.write(resp.read()); tmp.close()
with zipfile.ZipFile(tmp.name) as zf:
zf.extractall(site_packages)
os.unlink(tmp.name)
return True
print(f'[FAIL] {name}')
return False
然后正常使用:
# 安装依赖
for pkg in ['pytz', 'six', 'python-dateutil', 'numpy', 'pandas']:
install(pkg)
# 使用
import numpy as np
import pandas as pd
print(np.array([1, 2, 3]) * 10)
print(pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]}))
运行
把上面的代码保存为 script.py,然后:
wasmer run python/python@3.13 --net -- script.py
--net 参数开启网络访问,否则无法下载 wheel。依赖下载到哪了
wheel 解压后去了哪?看代码里的 site_packages 变量:
site_packages = os.path.join(
sys.base_exec_prefix, 'lib',
f'python{sys.version_info.major}.{sys.version_info.minor}',
'site-packages'
)
sys.base_exec_prefix 在 Wasmer Python 里是 /usr/local,所以最终路径是:
/usr/local/lib/python3.13/site-packages/
这个路径在 WASM 虚拟机的文件系统里,不在你的宿主机上。每次 wasmer run 都是一个全新的环境,上次装的包不会保留。
如果你希望依赖持久化,可以把 site-packages 挂载到宿主机目录:
# 第一次运行,装依赖
wasmer run python/python@3.13 --net \
--volume ./site-packages:/usr/local/lib/python3.13/site-packages \
-- script.py
# 之后运行,直接用已装好的依赖
wasmer run python/python@3.13 \
--volume ./site-packages:/usr/local/lib/python3.13/site-packages \
-- script.py
这样 wheel 会被解压到宿主机的 ./site-packages/ 目录,下次运行时直接挂载即可,不需要重新下载。
完整示例
把依赖安装和脚本运行分开,第一次装依赖,之后直接跑:
# 第一步:装依赖(写入宿主机 ./site-packages/)
wasmer run python/python@3.13 --net \
--volume ./site-packages:/usr/local/lib/python3.13/site-packages \
-- -c "
import sys, os, zipfile, urllib.request, re, tempfile, json
site_packages = '/usr/local/lib/python3.13/site-packages'
os.makedirs(site_packages, exist_ok=True)
def install(name):
try:
url = f'https://pythonindex.wasix.org/simple/{name}/'
html = urllib.request.urlopen(url, timeout=30).read().decode()
links = re.findall(r'href=\"([^\"]+cp313-cp313-wasix_wasm32\.whl[^\"]*)\"', html)
if links:
target = 'https://pythonindex.wasix.org/packages/' + links[0].split('/')[-1].split('#')[0]
print(f'[wasix] {name}...')
resp = urllib.request.urlopen(target, timeout=120)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.whl')
tmp.write(resp.read()); tmp.close()
with zipfile.ZipFile(tmp.name) as zf: zf.extractall(site_packages)
os.unlink(tmp.name)
return True
except: pass
data = json.loads(urllib.request.urlopen(f'https://pypi.org/pypi/{name}/json').read())
ver = data['info']['version']
for f in data['releases'].get(ver, []):
fn = f['filename']
if 'py3-none-any' in fn and fn.endswith('.whl'):
print(f'[pure] {name} {ver}...')
resp = urllib.request.urlopen(f['url'], timeout=60)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.whl')
tmp.write(resp.read()); tmp.close()
with zipfile.ZipFile(tmp.name) as zf: zf.extractall(site_packages)
os.unlink(tmp.name)
return True
print(f'[FAIL] {name}')
for pkg in ['pytz', 'six', 'python-dateutil', 'numpy', 'pandas']:
install(pkg)
"
# 第二步:运行脚本(直接用已装好的依赖)
wasmer run python/python@3.13 \
--volume ./site-packages:/usr/local/lib/python3.13/site-packages \
--volume ./script.py:/script.py \
-- script.py
为什么 pip 不能直接用
WASIX 构建的 Python 缺少 mmap 模块(pip 内部依赖它来做缓存),所以 pip install 跑不了。但 ensurepip 模块是有的,可以提取出 pip 本身——只是 install 命令不可用。
版本注意事项
- 需要用
python/python@3.13,因为 WASIX 索引的 wheel 都是cp313的 wasmer/python是 3.12 版本,对应的 WASIX wheel 较少- 如果包在 WASIX 索引里找不到,那它要么没有 C 扩展(走 PyPI 纯 Python wheel),要么就是不支持
总结
在 Wasmer 里跑 Python 并安装依赖的关键:
- 不用 pip,用
urllib+zipfile手动下载解压 wheel - 含 C 扩展的包从
pythonindex.wasix.org下载 - 纯 Python 包从 PyPI 下载
py3-none-anywheel - 运行时加
--net开启网络 - 用
--volume把site-packages挂载到宿主机,实现依赖持久化
这样就能在完全不需要本地 Python 环境的情况下,运行任何 Python 脚本了。