使用嵌入式子命令为Coni CLI提供超级加速

Coni语言最大的优势之一是其便携性。我们的设计使得核心解释器和标准库作为单一的静态二进制文件发布。您不需要臃肿的安装过程;只需下载可执行文件即可开始使用。

然而,随着生态系统的不断发展(例如添加了我们的Android构建流水线),我们发现工作流中出现了一个小烦恼。要调用Android APK构建器,您必须通过其绝对路径运行脚本:

coni libs/android/bin/build-apk.coni ./my-app

虽然它能用,但感觉不够原生。我们希望提供一种极致的开发者体验,让开发者可以简单地运行:

coni android build-apk ./my-app

今天,我们非常高兴地宣布,我们通过实现由Go的 embed 文件系统驱动的动态子命令路由 (Dynamic Subcommand Routing),成功填补了这一空白!

它是如何工作的

我们对Go编译器进行了三个关键的架构级修改:

1. 嵌入二进制文件

我们扩展了Go编译器内部的 //go:embed 指令,以自动打包所有模块中的所有 bin/ 目录:

//go:embed libs/*/src libs/*/bin
var embeddedLibs embed.FS

这个简单的改变保证了辅助脚本(如 build-apk.coni)被永久地“烧录”到可执行文件中。

2. 子命令拦截器

我们在CLI参数解析逻辑中引入了一个巧妙的拦截器。现在,如果您运行一个无法识别的命令,比如 coni android build-apk,解析器会拦截它并动态构建一个路径查询:libs/android/bin/build-apk.coni

它会检查嵌入的文件系统,如果该脚本存在,它会动态地将执行直接重新路由到嵌入的有效载荷!

3. 参数掩码屏蔽

最后一个拼图是保持脚本自身的“错觉”。build-apk.coni 脚本使用 cli/parse-opts 来读取标志(如 -p camera)。如果我们盲目地将参数传递给评估器,脚本会被前面的 androidbuild-apk 令牌搞糊涂。

为了解决这个问题,Go路由器在幕后重写了全局的 os.Args 切片:

os.Args = append([]string{os.Args[0], embeddedPath}, os.Args[3:]...)

从脚本的角度来看,它认为自己是被直接调用的!

结果

其结果就是获得了闪电般快速、无缝的CLI体验。所有的工具和辅助脚本都有机地打包在Coni二进制文件中,通过自然、感觉像原生的子命令即可瞬间访问。

没有额外的下载,没有路径配置。只有纯粹、不受干扰的生产力!

在Coni中构建原生并行下载

构建自己的语言和工具的绝佳之处之一,是能够重新思考操作的执行方式。在最近的提交中,我们决定解决构建过程中的一个主要瓶颈:下载Maven依赖项。

特定平台脚本存在的问题

以前,Coni中的 download-url-to-file 函数依赖于调用特定平台的外部工具:

  • 在Linux/macOS上,它会生成一个 curl 进程。
  • 在Windows上,它会调用一个使用 System.Net.WebClient 的庞大 powershell 命令。

虽然这种方法可行,但它有几个缺点。首先,调用外部进程既缓慢又消耗资源。其次,依赖外部工具意味着 curl 版本的微妙差异或Windows安全协议可能会导致意想不到的失败。最重要的是,使用这些shell命令顺序下载工件意味着解析大型Maven项目会花费太长时间。

原生解决方案

在提交 9ac76d12 中,我们引入了 sys-http-download,这是一个直接在Go评估器中实现的原生内置函数。这个新的内置函数使用了Go标准的 net/http 客户端。

因为它原生内置在评估器中,所以我们完全避免了进程创建的开销。我们甚至在Go实现中直接加入了一些健壮的重试逻辑,以处理来自Maven Central的速率限制(如果您在没有可识别的User-Agent的情况下请求过快,它经常会抛出429 Too Many Requests错误)。

使用Goroutine进行超级加速

在原生内置函数准备就绪后,真正的魔力发生在我们应用Coni的并发原语时。Coni使用 gochan<!(从通道读取)和 >!(写入通道)原生支持Go风格的并发。

我们没有逐个下载工件,而是直接在标准库中设置了一个工作池 (worker pool):

(let [total-count (count missing)
      result-ch (chan total-count)
      task-ch (chan total-count)]
  ;; 将所有任务推送到任务通道
  (loop [rem missing]
    (if (not (empty? rem))
      (do 
        (>! task-ch (first rem))
        (recur (rest rem)))))
  (close! task-ch)
  
  ;; 生成8个工作goroutine并发处理任务
  (loop [i 0]
    (if (< i 8)
      (do
        (go 
          (loop []
            (let [item (<! task-ch)]
              (if (not (nil? item))
                (do
                  (>! result-ch (download-file-single item repos))
                  (recur))))))
        (recur (+ i 1)))))

  ;; 等待结果并显示进度
  ...)

我们将工作池的上限限制为8个goroutine,以防止网络不堪重负或触发激进的速率限制。