主流程:
完成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
}