45个版本 (破坏性)

0.47.6 2024年2月22日
0.47.5 2023年6月18日
0.47.3 2023年5月23日
0.46.2 2023年3月8日
0.29.0 2019年7月11日

#207 in 开发工具

MIT 许可证

230KB
5K SLoC

Toast 🥂

Build status

Toast 是一个用于将您的构建和测试等工作流程容器化的工具。您在名为 toastfile 的 YAML 文件中定义任务,Toast 将它们在您选择的 Docker 镜像中运行。构成“任务”的内容由您决定:任务可以安装系统包、编译应用程序、运行测试套件,甚至提供网页服务。任务可以依赖于其他任务,因此 Toast 可以被视为一个高级的容器化构建系统。

Welcome to Toast.

以下是上述示例的 toastfile

image: ubuntu
tasks:
  install_gcc:
    command: |
      apt-get update
      apt-get install --yes gcc

  build:
    dependencies:
      - install_gcc
    input_paths:
      - main.c
    command: gcc main.c

  run:
    dependencies:
      - build
    command: ./a.out

Toast 通过将容器提交为镜像来缓存每个任务。该镜像被标记为任务的 shell 命令的加密哈希值、复制到容器中的文件内容以及所有其他任务输入。此哈希值允许 Toast 跳过自上次运行以来未更改的任务。

除了本地缓存之外,Toast 还可以使用 Docker 仓库作为远程缓存。您、您的团队成员以及您的持续集成(CI)系统都可以共享相同的远程缓存。以这种方式使用,您的 CI 系统可以执行所有繁重的工作,例如构建和安装依赖项,这样您和您的团队能够专注于开发。

相关工具

  • Docker Compose: Docker Compose 是一个方便的基于 Docker 的开发环境,与 Toast 共享许多功能。然而,它不支持定义任务(如 linttestrun 等)或远程缓存。
  • Nix: Nix 通过利用函数式编程的概念而不是容器化来实现可重复构建。我们是 Nix 的大粉丝。然而,与 Toast 相比,Nix 需要更大的承诺,因为您必须使用 Nix 包管理器或编写自己的 Nix 派生。无论好坏,Toast 允许您使用熟悉的语法,如 apt-get install ...

为了防止在使用Toast或Docker Compose等Docker相关工具时,在您的机器上积累Docker镜像,我们建议使用Docuum来执行最少最近使用(LRU)镜像淘汰。

教程

定义一个简单的任务

让我们创建一个toastfile。创建一个名为toast.yml的文件,内容如下

image: ubuntu
tasks:
  greet:
    command: echo 'Hello, World!' # Toast will run this in a container.

现在运行toast。你应该会看到以下内容

Defining a simple task.

如果你再次运行它,Toast会找到没有任何变化并跳过任务

Caching.

Toast缓存任务以节省你的时间。例如,你不需要每次运行测试时都重新安装你的依赖项。但是,缓存可能不适合某些任务,如运行开发服务器。你可以使用cache选项禁用特定任务的缓存以及所有依赖于它的任务

image: ubuntu
tasks:
  greet:
    cache: false # Don't cache this task.
    command: echo 'Hello, World!'

添加依赖项

让我们使用名为figlet的程序让问候更有趣。我们将添加一个安装figlet的任务,并将greet任务改为依赖于它

image: ubuntu
tasks:
  install_figlet:
    command: |
      apt-get update
      apt-get install --yes figlet

  greet:
    dependencies:
      - install_figlet # Toast will run this task first.
    command: figlet 'Hello, World!'

运行toast来看到一个惊人的问候

Adding a dependency.

从主机导入文件

这里有一个更实际的例子。假设你想编译和运行一个简单的C程序。创建一个名为main.c的文件

#include <stdio.h>

int main(void) {
  printf("Hello, World!\n");
  return 0;
}

更新toast.yml来编译和运行程序

image: ubuntu
tasks:
  install_gcc:
    command: |
      apt-get update
      apt-get install --yes gcc

  build:
    dependencies:
      - install_gcc
    input_paths:
      - main.c # Toast will copy this file into the container before running the command.
    command: gcc main.c

  run:
    dependencies:
      - build
    command: ./a.out

注意build任务中的input_paths数组。在这里,我们将一个文件复制到容器中,但我们可以用.导入包含toastfile的整个目录。默认情况下,文件将被复制到容器中的/scratch目录。命令将在该目录中运行。

现在,如果你运行toast,你会看到以下内容

Importing files from the host.

对于后续的运行,如果没有任何变化,Toast将跳过任务。但如果你更新了main.c中的问候,Toast将在下一次调用时检测到更改并重新运行buildrun任务。

从容器导出文件

Toast的一个常见用例是构建项目。当然,你可能想知道如何从主机机器访问容器内部产生的构建工件。使用output_paths很容易做到

image: ubuntu
tasks:
  install_gcc:
    command: |
      apt-get update
      apt-get install --yes gcc

  build:
    dependencies:
      - install_gcc
    input_paths:
      - main.c
    output_paths:
      - a.out # Toast will copy this file onto the host after running the command.
    command: gcc main.c

当Toast运行build任务时,它将a.out文件复制到主机。

Exporting files from the container.

向任务传递参数

有时,任务需要接受参数。例如,一个deploy任务可能想知道你是否想部署到stagingproduction集群。为此,向你的任务添加一个environment部分

image: ubuntu
tasks:
  deploy:
    cache: false
    environment:
      CLUSTER: staging # Deploy to staging by default.
    command: echo "Deploying to $CLUSTER..."

当你运行这个任务时,Toast将读取环境中的值

Passing arguments to a task.

如果变量不存在于环境中,Toast将使用默认值

Using argument defaults.

如果你想不设置默认值,将其设置为null

image: ubuntu
tasks:
  deploy:
    cache: false
    environment:
      CLUSTER: null # No default; this variable must be provided at runtime.
    command: echo "Deploying to $CLUSTER..."

现在,如果你不指定CLUSTER而运行toast deploy,Toast会抱怨变量缺失并拒绝运行任务。

任务中列出的环境变量也适用于任何在其之后运行的任务。

运行服务器并将路径挂载到容器中

Toast不仅可以用于构建项目。假设你正在开发一个网站。你可以定义一个Toast任务来运行你的web服务器!创建一个名为index.html的文件,内容如下

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to Toast!</title>
  </head>
  <body>
    <p>Hello, World!</p>
  </body>
</html>

我们可以使用像nginx这样的Web服务器。官方的nginx Docker镜像就足够了,但您也可以使用更通用的镜像,并定义一个Toast任务来安装nginx。

在我们的toast.yml文件中,我们将使用ports字段使网站在容器外部可访问。我们还将使用mount_paths而不是input_paths,这样我们就可以在不重新启动服务器的情况下编辑网页。

image: nginx
tasks:
  serve:
    cache: false # It doesn't make sense to cache this task.
    mount_paths:
      - index.html # Updates to this file will be visible inside the container.
    ports:
      - 3000:80 # Expose port 80 in the container as port 3000 on the host.
    location: /usr/share/nginx/html/ # Nginx will serve the files in here.
    command: nginx -g 'daemon off;' # Run in foreground mode.

现在您可以使用Toast运行服务器

Running a server.

配置shell

在运行任何命令之前,通常需要以某种方式配置shell。shell通常使用所谓的“启动文件”进行配置(例如,~/.bashrc)。然而,许多shell在以非交互式、非登录模式运行时跳过了加载此类配置文件,这是Toast调用shell的方式。Toast提供了一个替代机制来配置shell,而无需创建任何特殊文件或以特定方式调用shell。

考虑以下使用Bash作为shell的toastfile,因为这是Ubuntu中默认首选的登录shell

image: ubuntu
tasks:
  install_figlet:
    command: |
      apt-get update
      apt-get install --yes figlet

如果apt-get update失败会发生什么?由于Bash的工作方式,失败将被忽略,并继续执行后续行。您可以使用以下方式使用set -e进行修复

image: ubuntu
tasks:
  install_figlet:
    command: |
      set -e # Make Bash fail fast.
      apt-get update
      apt-get install --yes figlet

但是,将其分别添加到每个任务中是繁琐且容易出错的。相反,您可以通过以下方式一次将此添加到每个任务中,设置command_prefix

image: ubuntu
command_prefix: set -e # Make Bash fail fast.
tasks:
  install_figlet:
    command: |
      apt-get update
      apt-get install --yes figlet

对于Bash来说,我们建议更进一步,设置set -euo pipefail而不是仅仅设置set -e

进入交互式shell

如果您以--shell运行Toast,当请求的任务完成时,或者如果其中任何一个任务失败,Toast将将您放入容器内的交互式shell。此功能对于调试任务或探索容器中的内容非常有用。假设您有以下toastfile

image: ubuntu
tasks:
  install_figlet:
    command: |
      apt-get update
      apt-get install --yes figlet

您可以使用toast --shell来玩figlet程序

Dropping into a shell.

完成操作后,容器将自动删除。

Toast是如何工作的

给定要运行的一组任务,Toast计算依赖DAG的拓扑排序,以确定任务的运行顺序。然后Toast根据拓扑排序中上一个任务的镜像构建每个任务的Docker镜像,或对于第一个任务,使用基础镜像。

任意DAG的拓扑排序不一定唯一。Toast使用基于深度优先搜索的算法,按字典顺序遍历子节点。该算法是确定性的,与任务和依赖列表的顺序无关,因此重新排序toastfile中的任务不会使缓存无效。此外,toast foo bartoast bar foo将保证产生相同的计划,以最大程度地利用缓存。

对于计划中的每个任务,Toast首先根据shell命令的哈希值、input_paths的内容、计划中上一个任务的缓存键等计算一个缓存键。然后Toast将寻找带有该缓存键标记的Docker镜像。如果找到该镜像,Toast将跳过该任务。否则,Toast将创建一个容器,将任何input_paths复制到其中,运行shell命令,将任何output_paths从容器复制到主机,将容器提交到镜像,并删除容器。该镜像带有缓存键标记,以便任务可以在后续运行中跳过。

Toast旨在尽可能少地对容器环境做出假设。Toast仅假设存在一个程序在/bin/su,它可以以su -c COMMAND USER的方式调用。该程序用于以适当用户及其首选的shell运行容器中的命令。每个流行的Linux发行版都有一个支持此用法的su实用程序。Toast包含集成测试,以确保它能与流行的基本镜像(如debianalpinebusybox等)一起工作。

Toastfile参考

一个toastfile是一个YAML文件(通常命名为toast.yml),用于定义任务及其依赖关系。该架构包含以下顶级键和默认值

image: <required>   # Docker image name with optional tag or digest
default: null       # Name of default task to run or `null` to run all tasks by default
location: /scratch  # Path in the container for running tasks
user: root          # Name of the user in the container for running tasks
command_prefix: ''  # A string to be prepended to all commands by default
tasks: {}           # Map from task name to task

任务具有以下架构和默认值

description: null           # A description of the task for the `--list` option
dependencies: []            # Names of dependencies
cache: true                 # Whether a task can be cached
environment: {}             # Map from environment variable to optional default
input_paths: []             # Paths to copy into the container
excluded_input_paths: []    # A denylist for `input_paths`
output_paths: []            # Paths to copy out of the container if the task succeeds
output_paths_on_failure: [] # Paths to copy out of the container if the task fails
mount_paths: []             # Paths to mount into the container
mount_readonly: false       # Whether to mount the `mount_paths` as readonly
ports: []                   # Port mappings to publish
location: null              # Overrides the corresponding top-level value
user: null                  # Overrides the corresponding top-level value
command: ''                 # Shell command to run in the container
command_prefix: null        # Overrides the corresponding top-level value
extra_docker_arguments: []  # Additional arguments for `docker container create`

Toast自身的toastfile是一个全面的实际示例。

配置

Toast可以通过YAML配置文件进行自定义。配置文件的默认位置取决于操作系统

  • 对于macOS,默认位置是$HOME/Library/Application Support/toast/toast.yml
  • 对于其他Unix平台,Toast遵循XDG Base Directory Specification。默认位置是$XDG_CONFIG_HOME/toast/toast.yml$HOME/.config/toast/toast.yml,如果XDG_CONFIG_HOME未设置为绝对路径。
  • 对于Windows,默认位置是{FOLDERID_RoamingAppData}\toast\toast.yml

配置文件的架构将在下面的子节中描述。

缓存配置

Toast支持本地和远程缓存。默认情况下,仅启用本地缓存。远程缓存需要Docker Engine登录到Docker注册库(例如,通过docker login)。

以下是与缓存相关的字段及其默认值

docker_repo: toast        # Docker repository
read_local_cache: true    # Whether Toast should read from local cache
write_local_cache: true   # Whether Toast should write to local cache
read_remote_cache: false  # Whether Toast should read from remote cache
write_remote_cache: false # Whether Toast should write to remote cache

每个选项都可以通过命令行选项覆盖(见下文)。

对于CI环境,通常将启用所有形式的缓存,而对于本地开发,您可能希望设置write_remote_cache: false以避免等待远程缓存写入。

Docker CLI

您可以配置Toast使用的Docker CLI二进制文件。Toast使用PATH环境变量来搜索指定的二进制文件。您可以使用此机制切换到Docker CLI的替换品,如Podman。

以下是与之相关的字段及其默认值

docker_cli: docker

命令行选项

默认情况下,Toast会在工作目录中查找名为toast.yml的toastfile,然后是父目录,依此类推。toastfile中的任何路径都是相对于toastfile的位置,而不是工作目录。这意味着您可以从项目的任何位置运行Toast并获得相同的结果。

无参数运行toast以执行默认任务,或者如果toastfile未定义默认任务,则执行所有任务。您还可以执行特定的任务及其依赖关系

toast task1 task2 task3…

以下都是支持的命令行选项

USAGE:
    toast [OPTIONS] [--] [TASKS]...

OPTIONS:
    -c, --config-file <PATH>
            Sets the path of the config file

        --docker-cli <CLI>
            Sets the Docker CLI binary

    -r, --docker-repo <REPO>
            Sets the Docker repository for remote caching

    -f, --file <PATH>
            Sets the path to the toastfile

        --force <TASK>...
            Runs a task unconditionally, even if it’s cached

        --force-all
            Pulls the base image and runs all tasks unconditionally

    -h, --help
            Prints help information

    -l, --list
            Lists the tasks that have a description

    -o, --output-dir <PATH>
            Sets the output directory

        --read-local-cache <BOOL>
            Sets whether local cache reading is enabled

        --read-remote-cache <BOOL>
            Sets whether remote cache reading is enabled

    -s, --shell
            Drops you into a containerized shell after the tasks are finished

    -v, --version
            Prints version information

        --write-local-cache <BOOL>
            Sets whether local cache writing is enabled

        --write-remote-cache <BOOL>
            Sets whether remote cache writing is enabled


ARGS:
    <TASKS>...
            Sets the tasks to run

安装说明

在macOS或Linux(AArch64或x86-64)上的安装

如果您正在使用 macOS 或 Linux(AArch64 或 x86-64),可以使用以下命令安装 Toast

curl https://raw.githubusercontent.com/stepchowfun/toast/main/install.sh -LSfs | sh

相同的命令可以再次使用来更新到最新版本。

安装脚本支持以下可选环境变量

  • VERSION=x.y.z(默认为最新版本)
  • PREFIX=/path/to/install(默认为 /usr/local/bin

例如,以下命令将 Toast 安装到工作目录

curl https://raw.githubusercontent.com/stepchowfun/toast/main/install.sh -LSfs | PREFIX=. sh

如果您不希望使用此安装方法,可以从发布页面下载二进制文件,使用 chmod 使其可执行,并将其放置在您的 PATH(例如,/usr/local/bin)中的某个目录。

Windows(AArch64 或 x86-64)上的安装

如果您正在使用 Windows(AArch64 或 x86-64),请从发布页面下载最新二进制文件,并将其重命名为 toast(如果您文件扩展名可见,则为 toast.exe)。在您的 %PROGRAMFILES% 目录中创建一个名为 Toast 的目录(例如,C:\Program Files\Toast),并将重命名的二进制文件放在那里。然后,在控制面板的“系统属性”部分的“高级”选项卡中,单击“环境变量...”,并将新 Toast 目录的完整路径添加到“系统变量”下的 PATH 变量中。请注意,如果 Windows 配置为非英语语言,则“程序文件”目录可能有不同的名称。

要更新现有安装,只需替换现有二进制文件即可。

使用 Homebrew 安装

如果您有 Homebrew,可以按以下方式安装 Toast

brew install toast

您可以使用 brew upgrade toast 更新现有安装。

使用 MacPorts 安装

在 macOS 上,您还可以通过 MacPorts 安装 Toast,如下所示

sudo port install toast

您可以通过以下方式更新现有安装

sudo port selfupdate
sudo port upgrade toast

使用 Cargo 安装

如果您有 Cargo,可以按以下方式安装 Toast

cargo install toast

您可以使用 --force 运行该命令来更新现有安装。

在 CI 中运行 Toast

在 CI 中运行 Toast 的最简单方法是使用 GitHub Actions。Toast 提供了一个方便的 GitHub action,您可以在您的 workflows 中使用。以下是一个简单的 workflow,其中 Toast 无参数运行

# .github/workflows/ci.yml
name: Continuous integration
on:
  pull_request:
  push:
    branches:
    - main
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: stepchowfun/toast/.github/actions/toast@main

以下是一个展示所有选项的更定制化的 workflow

# .github/workflows/ci.yml
name: Continuous integration
on:
  pull_request:
  push:
    branches:
    - main
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - if: github.event_name == 'push'
      uses: docker/login-action@v3
      with:
        username: DOCKER_USERNAME
        password: ${{ secrets.DOCKER_PASSWORD }}
    - uses: stepchowfun/toast/.github/actions/toast@main
      with:
        file: toastfiles/toast.yml
        tasks: build lint test
        docker_repo: DOCKER_USERNAME/DOCKER_REPO
        read_remote_cache: true
        write_remote_cache: ${{ github.event_name == 'push' }}

要求

  • Toast 需要 17.06.0 或更高版本的 Docker Engine
  • Toast 只与 Linux 容器一起工作;目前不支持 Windows 容器。然而,Toast 除了 Linux 主机外,还支持具有适当虚拟化能力的 macOS 和 Windows 主机,这归功于 Docker Desktop

致谢

Toast 是受到 Airbnb CI 作业中使用的内部工具的启发。设计受到了我在开发该工具和与出色的 CI 基础设施团队一起构建 Airbnb 的 CI 系统时所学到的大量教训的影响。

特别感谢Julia Wang(《@juliahw》)提供的宝贵早期反馈。感谢她和Mark Tai(《@marktai》)想出了《Toast》这个名字。

终端动画是用asciinemasvg-term-cli制作的。

依赖项

~10–22MB
~308K SLoC