PyStudio Termux 构建踩坑记(四):Android 私有目录里的路径、npm、pip 和 proot
这篇讲的是 PyStudio 运行时最容易被低估的一类问题:文件都在,命令也能看到,但脚本、解释器、动态库和 prefix 仍然可能指向错误位置。Termux 官方环境里能跑,不代表搬到 Android App 私有目录后还能跑。 我把它拆成几条线:shell 内建命令和外部 ELF 的差异、npm/pip 动态产物的 shebang 兼容、自家包和官方源包的边界,以及 proot 这种 syscall 敏感工具为什么必须单独验收。 只要 prefix 变了,脚本、wrapper、ELF loader 和动态库路径都可能跟着出问题。 为什么 pwd 和 cd 正常,ls 和 pkg 却卡住最早的现象很迷惑:新 app 里运行 pwd、cd 反应很快,但 ls、pkg 回车后没有反应。 后来回头看,这个现象其实很典型: pwd、cd 多数时候是 shell 内建命令。 ls 是外部 ELF。 pkg 是脚本和包管理逻辑。 内建命令能跑,只能证明 shell 基本活着。外部命令卡住,可能是: ELF loader 路径不对。 动态库查找路径不对。 sheban...
PyStudio Termux 构建踩坑记(三):Gitee、ModelScope 和 Cloudflare Pages 清单站
这篇讲下载入口怎么从“找个国内仓库放一下”变成一套更清楚的镜像架构。最早我想用 Gitee 做国内镜像,后来发现它适合轻量文本,不适合承载大量 release 附件和工具链包。 最后的分工更明确:GitHub Release 保存权威产物,ModelScope 承担大文件镜像,专用清单仓库保存 JSON,Cloudflare Pages 对外提供清单站。清单越轻,大文件镜像越独立,后面才容易按区域优化。 Gitee 的问题不是不能用,而是不适合在这套系统里承担大文件镜像角色。 Gitee 方案为什么失败一开始选 Gitee 的理由很直接:大陆访问速度更好,轻量 JSON 清单应该很适合放上去。 实际踩坑后发现,Gitee release 附件有几个限制: 单个附件大小有限制。 仓库附件总容量有限制。 大量 .deb 或工具链压缩包会很快逼近上限。 上传失败后重试成本高。 比如 C/C++ 工具链压缩包很容易超过 100 MiB。即使某些项目等级能放到更大的单文件,也挡不住总容量限制。把构建产物全推到 Gitee,不是长期方案。 所以 Gitee 只适合“轻量...
PyStudio Termux 构建踩坑记(二):像 Termux pkg 一样拆包,而不是发布工具链大包
这篇是 PyStudio Termux 构建系列的第二篇,主题是包管理模型。工具链大包一开始很诱人:用户要 Python 就下 python-toolchain,要 Node 就下 node-toolchain。但很快会遇到重复依赖、粗粒度更新、构建失败难定位和版本不可追踪。 最后更接近 Termux/TUR 的路线:一个包一个版本,四个架构对应四个 .deb,每个架构有自己的 Packages.xz。App 侧不必完整复刻 apt,但至少要有一个能解析依赖、跳过已安装包、校验下载结果的轻量 resolver。 文件数量变多不是坏事,失去索引才是坏事。 大包方案的坑大包方案一开始很自然: 1234python-toolchain-aarch64.tar.gznode-toolchain-aarch64.tar.gzcpp-toolchain-aarch64.tar.gzdebug-tools-aarch64.tar.gz 用户需要 Python,就下载 python-toolchain。需要 Node,就下载 node-toolchain。看起来简单,app ...
PyStudio Termux 构建踩坑记(一):从 bootstrap 大包到可维护构建体系
这篇是 PyStudio Termux 构建系列的第一篇,也是一开始方向转弯最大的一篇。最初我们想直接打一个“什么都有”的 bootstrap,后来发现能编译一次不等于能长期维护。真正的问题不是 CI 多跑几次,而是 bootstrap 承担了太多职责。 后来路线变成:bootstrap 只做基础环境,工具链拆成可选包,包索引负责依赖,清单站负责发现,大文件镜像负责下载。这条线一旦拆清楚,后面 Python LSP、C++、debug、proot 才有地方放。 这张图是这个系列的底图:bootstrap 越小,后续包管理和镜像策略越有空间。 最早的问题:能编译不等于能长期维护最开始的目标很朴素:给 PyStudio 打出一个能在 Android App 私有目录里运行的 Termux 风格环境。能运行 pwd、cd、ls、pkg、python、pip、node,再逐步加上 C/C++、LSP、debugger、proot、distro 这些能力。 早期方案有几个典型坑: bootstrap 成功解压,不代表命令都能跑。 pwd、cd 这类 shell 内建命令...
Sora Editor 上做代码高亮和纠错:Tree-sitter、LSP 与终端包的边界
PyStudio 编辑器这条线最容易混淆三个东西:APK 里给 Sora Editor 用的 Tree-sitter native library、终端里用户能运行的 tree-sitter CLI,以及真正做诊断、补全、Quick Fix 的 LSP server。 它们都和“代码智能”有关,但不是一层能力。终端里 tree-sitter --version 能跑,不代表编辑器高亮生效;Tree-sitter 接好了,也不代表会有语义纠错。把这几条边界画清楚,后面才知道该修 APK native、运行时包,还是 editor-lsp。 这张图先把三层拆开:结构解析、终端工具、语义服务分别解决不同问题。 Tree-sitter 解决的是结构,不是语义Tree-sitter 很适合做增量解析和结构化高亮。对编辑器来说,它能回答: 这是关键字还是字符串; 当前光标在函数、参数、代码块还是注释里; 改了一行后如何增量更新语法树; 是否能基于语法节点做缩进和简单补全。 但它通常不能单独回答: 这个变量有没有定义; 这个函数参数类型是否正确; 这个 import 是否缺包; ...
做 Android IDE 终端时踩过的 UI 坑:键盘、会话、侧滑栏和文件管理
PyStudio 这类 Android IDE 最容易被低估的,不是“能不能跑一条命令”,而是终端、编辑器、键盘和文件管理几条线能不能互相不添乱。键盘弹起时符号栏错位、侧滑栏误触、运行脚本后回不到编辑器、文件列表钻进空目录,这些问题单独看都小,叠在一起就会让 App 像临时 demo。 这篇更像一次 UI 复盘:哪些状态应该交给终端运行时,哪些交给页面,哪些交互要在手机上收紧。读完至少能得到一条判断线:移动端 IDE 的体验,不靠一个惊艳控件,而靠很多边界都稳定。 这张图先把终端会话、编辑器、文件系统和输入法放到同一个工作台上,后面的几个坑基本都来自这些边界被写乱。 终端不能只是一个 View一开始终端逻辑很容易写成: 1234TerminalPage -> ViewModel -> create TerminalSession -> attach TerminalView 这样能跑,但生命周期很脆。Activity 重建、页面切换、安装脚本运行、Python 调试会话、Alpine/proot 会话都混在 ViewModel 里,最后...
把 Android IDE 的运行时依赖做成清单:PyStudio 包管理踩坑记录
PyStudio 不可能把 Python、pip、debug 工具、tree-sitter、proot、Alpine rootfs、C/C++ 工具链全塞进 APK。App 本体要轻,运行时能力又要能按需安装和更新,这中间必须有一层清单。 这篇记录我最后采用的模型:runtime-packages.json 负责发现能力,Packages.xz 负责包级依赖,planner 负责算安装计划,installer 把计划变成用户能看见的终端脚本。 清单不是另一个 apt。它负责告诉 App 去哪里找能力,真正的依赖关系仍然交给包索引。 为什么不用一堆写死链接最早的想法很简单:App 里放一个按钮,点了就下载某个包。问题也很快出现: 不同 ABI 的包不同:aarch64、arm、i686、x86_64; 有些能力不是一个文件,而是一组 .deb 依赖; 同一个 profile 会不断重建,比如 proot 从 r40 到 r100; bootstrap 也需要灵活更新,不能永远内置在 APK; 下载源会变:GitHub、Gitee、ModelScope、Cloud...
Android App 里跑 proot:一次 fork 被 seccomp 拦截后的排查复盘
这次问题最容易误判:同一份 Alpine rootfs、同一个 proot 二进制,在 adb run-as 里能跑,到了 App 真实终端里却报 /bin/sh: can't fork: Function not implemented。如果只看控制实验,很容易去重做 rootfs、下载脚本或 UI。 真正的分界线在 Android App 进程域。adb run-as 成功只能说明文件和参数大体没坏,不能证明 zygote/seccomp/SELinux 这条真实路径也通。这篇按排查顺序写,重点是如何把验收线放回 App 域里。 左边是容易误导人的控制组,右边才是 App 内终端真正会走到的路径。 一开始误判的地方最容易误判的是这条控制实验: 1234567adb shell run-as com.vchangxiao.pystudio ` /data/data/com.vchangxiao.pystudio/files/usr/bin/proot ` -r /data/data/com.vchangxiao.pystudio/file...
把膝关节超声 AI 部署到 Hugging Face Spaces:Gradio 6 项目的踩坑复盘
这篇是一次完整的 Hugging Face Spaces 部署复盘:一个本地 Gradio 应用,最后变成可公开访问、可通过 API 调用、可生成 Markdown/PDF 报告的 Space。项目本身用于膝关节超声图像里的髌腱/髌软骨识别、厚度测量和健康风险参考;它不是医疗诊断系统,定位是 AI 辅助健康参考。 我把坑按部署链路重新整理了一遍:模型文件怎么上传、页面和 API 如何共用同一条推理链、PDF 中文字体怎么兜底、深色模式为什么会翻车,以及远端 API 应该怎么验收。 项目地址: Hugging Face Space: vg188/knee-Ultrasound-agent 直接访问地址: https://vg188-knee-ultrasound-agent.hf.space 这张图把图像上传、模型推理和报告输出放在同一个工作台里。后面的每个坑,基本都落在其中一段。 最终形态最后部署出来的 Space 主要包含三层能力。 第一层是网页评估界面:用户上传膝关节超声图像,补充年龄、性别、BMI、活动水平、生活习惯、运动习惯、鞋履...
Cloudflare Pages + 华为云 DNS 分线路加速:原理和操作步骤
这篇记录一条已经实测跑通的加速路线:网站继续放在 Cloudflare Pages,父域 DNS 仍由 Cloudflare 管理,只把需要加速的子域名通过 NS 委派交给华为云 DNS,再让华为云 DNS 按线路返回不同的 CNAME。 重点不是“把 Cloudflare Pages 换掉”,而是把站点托管、父域管理、子域调度拆开。这样 Pages 自定义域名仍保持 active,大陆访问可以走优选 CNAME,其他地区还有默认线路兜底。 加速前后对比: 这张图是整篇的核心:Cloudflare 管父域和 Pages 归属,华为云 DNS 只管被委派的 speed.vg188.cyou。 技术路线和原理 不要把 Cloudflare DNS 里的 Pages CNAME 直接替换成优选域名;更稳的是先绑定 Pages,再把子域解析委派出去。本次测试用到的实际项目: 1234567Cloudflare Pages 项目名: av7980Pages 默认域名: av7980.pages.devPages 默认访问: https://av7980.pages.dev加速...