Earthly源码解析

主流程:

完成buildkit配置文件构建并启动,Earthly config文件的构建,环境变量的读取,根据传入的target参数解析执行earthfile为buildkit接收的llb state,由buildkit完成最终的执行。

主流程核心代码:

Earthly是通过命令行APP的方式来运行的,cmd/earthly/main.go是程序的入口,Earthly使用https://github.com/urfave/cli来构建命令行APP。

func (app *earthlyApp) actionBuildImp(c *cli.Context, flagArgs, nonFlagArgs []string) error {
    // 解析命令行传入的target(local or github)
    target, err = domain.ParseTarget(targetName)
    // 构建并启动buildkitd服务
    bkClient, err := buildkitd.NewClient(c.Context, app.console, app.buildkitdImage, app.containerName, app.buildkitdSettings)
    // 根据用户输入参数构建核心的Builder结构
    b, err := builder.NewBuilder(c.Context, builderOpts)
    // 执行target的构建
    _, err = b.BuildTarget(c.Context, target, buildOpts)
}

// 根据传入的target执行构建
func (b *Builder) BuildTarget(ctx context.Context, target domain.Target, opt BuildOpt) (*states.MultiTarget, error) {
    mts, err := b.convertAndBuild(ctx, target, opt)
    if err != nil {
        return nil, err
    }
    return mts, nil
}

func (b *Builder) convertAndBuild(ctx context.Context, target domain.Target, opt BuildOpt) (*states.MultiTarget, error) {
    bf := func(childCtx context.Context, gwClient gwclient.Client) (*gwclient.Result, error) {
        // 通过buildkit将传入的earthfile数据结构转化为buildkit能够使用的LLB states
        mts, err = earthfile2llb.Earthfile2LLB(....)
    }
    err := b.s.buildMainMulti(ctx, bf, onImage, onArtifact, onFinalArtifact, onPull, "main")
}

// 通过buildkit api,用Build call back的方式,将上边的bf方法(eathfile转成LLB)、solveOpt(定义输出格式)作为参数,执行真正的build过程。
func (s *solver) buildMainMulti(...) {
    _, err = s.bkClient.Build(ctx, *solveOpt, "", bf, ch)
}

// 将arthfile文件通过[antlr](https://github.com/antlr/antlr4/tree/master/runtime/Go/antlr)解析成spec.Earthfile结构,方便进一步解析成buildkit的LLB 
func (r *Resolver) parseEarthfile(ctx context.Context, path string) (spec.Earthfile, error) {
    ast.Parse(ctx, k.(string), true)
}

// Earthfile struct是Earthfile文件的AST语法树表示
type Earthfile struct {
    Version        *Version        `json:"version,omitempty"`
    BaseRecipe     Block           `json:"baseRecipe"`
    Targets        []Target        `json:"targets,omitempty"`
    UserCommands   []UserCommand   `json:"userCommands,omitempty"`
    SourceLocation *SourceLocation `json:"sourceLocation,omitempty"`
}

// Statement对应Command
type Statement struct {
    Command        *Command        `json:"command,omitempty"`
    With           *WithStatement  `json:"with,omitempty"`
    If             *IfStatement    `json:"if,omitempty"`
    For            *ForStatement   `json:"for,omitempty"`
    SourceLocation *SourceLocation `json:"sourceLocation,omitempty"`
}

// 对应Earthfile文件一行行的命令,如COPY RUN等
type Command struct {
    Name           string          `json:"name"`
    Args           []string        `json:"args"`
    ExecMode       bool            `json:"execMode,omitempty"`
    SourceLocation *SourceLocation `json:"sourceLocation,omitempty"`
}

// earthfile2llb/interpreter.go Interpreter结合Converter完成核心的语法树解析功能,将spec.Command解析成对应的buildkit LLB
//
// 将earthfile命令解析成buildkit的llb.State(LLB)
func (i *Interpreter) handleCommand(ctx context.Context, cmd spec.Command) (err error) {
    switch cmd.Name {
    case "FROM":
        return i.handleFrom(ctx, cmd)
    case "RUN":
        return i.handleRun(ctx, cmd)
        ...
        ...
}

解析实例:

earthfile文件:

FROM golang:1.15-alpine3.13
WORKDIR /go-example
deps:
    COPY go.mod go.sum ./
    RUN go mod download

earthfile文件解析转化成Earthfile结构ast语法树,对应Json格式如下:

{
  "baseRecipe":  [
    {
      "command": {
        "args": [
          "golang:1.15-alpine3.13"
        ],
        "name": "FROM"
      }
    }
  ],
  "targets": [
    {
      "name": "deps",
      "recipe": [
        {
          "command": {
            "name": "COPY",
            "args": [
              "go.mod", "go.sum", "./"
            ]
          }
        },
        {
          "command": {
            "name": "RUN",
            "args": ["go", "mod", "download"]
          }
        }
}

Earthly日志输出

Earthly 通过buildkit的 ch := make(chan *client.SolveStatus) SolveStatus 管道,同步的读取buildkit的输出结果(logs), 通过solverMonitor解析buildkit输出,并对输出进行加工,形成自己的日志输出信息。Earthly的日志输出相对buildkit原生日志更直观,更好理解。

func (sm *solverMonitor) monitorProgress(ctx context.Context, ch chan *client.SolveStatus, phaseText string, sideRun bool) (string, error) {
    for {
        select {
        case ss, ok := <-ch:
            if !ok {
                break Loop
            }
            err := sm.processStatus(ss)
            if err != nil {
                return "", err
            }
        case <-sm.noOutputTicker.C:
            err := sm.processNoOutputTick()
            if err != nil {
                return "", err
            }
        }
    }
}

type SolveStatus struct {
    Vertexes []*Vertex
    Statuses []*VertexStatus
    Logs     []*VertexLog
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容