1package builder 2 3import ( 4 "archive/tar" 5 "bytes" 6 "context" 7 "encoding/csv" 8 "encoding/json" 9 "fmt" 10 "net" 11 "path" 12 "regexp" 13 "strconv" 14 "strings" 15 16 "github.com/containerd/containerd/platforms" 17 controlapi "github.com/moby/buildkit/api/services/control" 18 "github.com/moby/buildkit/client/llb" 19 "github.com/moby/buildkit/exporter/containerimage/exptypes" 20 "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb" 21 "github.com/moby/buildkit/frontend/dockerfile/dockerignore" 22 "github.com/moby/buildkit/frontend/dockerfile/parser" 23 "github.com/moby/buildkit/frontend/gateway/client" 24 gwpb "github.com/moby/buildkit/frontend/gateway/pb" 25 "github.com/moby/buildkit/solver/errdefs" 26 "github.com/moby/buildkit/solver/pb" 27 "github.com/moby/buildkit/util/apicaps" 28 specs "github.com/opencontainers/image-spec/specs-go/v1" 29 "github.com/pkg/errors" 30 "golang.org/x/sync/errgroup" 31) 32 33const ( 34 DefaultLocalNameContext = "context" 35 DefaultLocalNameDockerfile = "dockerfile" 36 keyTarget = "target" 37 keyFilename = "filename" 38 keyCacheFrom = "cache-from" // for registry only. deprecated in favor of keyCacheImports 39 keyCacheImports = "cache-imports" // JSON representation of []CacheOptionsEntry 40 keyCacheNS = "build-arg:BUILDKIT_CACHE_MOUNT_NS" 41 defaultDockerfileName = "Dockerfile" 42 dockerignoreFilename = ".dockerignore" 43 buildArgPrefix = "build-arg:" 44 labelPrefix = "label:" 45 keyNoCache = "no-cache" 46 keyTargetPlatform = "platform" 47 keyMultiPlatform = "multi-platform" 48 keyImageResolveMode = "image-resolve-mode" 49 keyGlobalAddHosts = "add-hosts" 50 keyForceNetwork = "force-network-mode" 51 keyOverrideCopyImage = "override-copy-image" // remove after CopyOp implemented 52 keyNameContext = "contextkey" 53 keyNameDockerfile = "dockerfilekey" 54 keyContextSubDir = "contextsubdir" 55 keyContextKeepGitDir = "build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR" 56 keySyntax = "build-arg:BUILDKIT_SYNTAX" 57 keyHostname = "hostname" 58) 59 60var httpPrefix = regexp.MustCompile(`^https?://`) 61var gitURLPathWithFragmentSuffix = regexp.MustCompile(`\.git(?:#.+)?$`) 62 63func Build(ctx context.Context, c client.Client) (*client.Result, error) { 64 opts := c.BuildOpts().Opts 65 caps := c.BuildOpts().LLBCaps 66 gwcaps := c.BuildOpts().Caps 67 68 allowForward, capsError := validateCaps(opts["frontend.caps"]) 69 if !allowForward && capsError != nil { 70 return nil, capsError 71 } 72 73 marshalOpts := []llb.ConstraintsOpt{llb.WithCaps(caps)} 74 75 localNameContext := DefaultLocalNameContext 76 if v, ok := opts[keyNameContext]; ok { 77 localNameContext = v 78 } 79 80 forceLocalDockerfile := false 81 localNameDockerfile := DefaultLocalNameDockerfile 82 if v, ok := opts[keyNameDockerfile]; ok { 83 forceLocalDockerfile = true 84 localNameDockerfile = v 85 } 86 87 defaultBuildPlatform := platforms.DefaultSpec() 88 if workers := c.BuildOpts().Workers; len(workers) > 0 && len(workers[0].Platforms) > 0 { 89 defaultBuildPlatform = workers[0].Platforms[0] 90 } 91 92 buildPlatforms := []specs.Platform{defaultBuildPlatform} 93 targetPlatforms := []*specs.Platform{nil} 94 if v := opts[keyTargetPlatform]; v != "" { 95 var err error 96 targetPlatforms, err = parsePlatforms(v) 97 if err != nil { 98 return nil, err 99 } 100 } 101 102 resolveMode, err := parseResolveMode(opts[keyImageResolveMode]) 103 if err != nil { 104 return nil, err 105 } 106 107 extraHosts, err := parseExtraHosts(opts[keyGlobalAddHosts]) 108 if err != nil { 109 return nil, errors.Wrap(err, "failed to parse additional hosts") 110 } 111 112 defaultNetMode, err := parseNetMode(opts[keyForceNetwork]) 113 if err != nil { 114 return nil, err 115 } 116 117 filename := opts[keyFilename] 118 if filename == "" { 119 filename = defaultDockerfileName 120 } 121 122 var ignoreCache []string 123 if v, ok := opts[keyNoCache]; ok { 124 if v == "" { 125 ignoreCache = []string{} // means all stages 126 } else { 127 ignoreCache = strings.Split(v, ",") 128 } 129 } 130 131 name := "load build definition from " + filename 132 133 filenames := []string{filename, filename + ".dockerignore"} 134 135 // dockerfile is also supported casing moby/moby#10858 136 if path.Base(filename) == defaultDockerfileName { 137 filenames = append(filenames, path.Join(path.Dir(filename), strings.ToLower(defaultDockerfileName))) 138 } 139 140 src := llb.Local(localNameDockerfile, 141 llb.FollowPaths(filenames), 142 llb.SessionID(c.BuildOpts().SessionID), 143 llb.SharedKeyHint(localNameDockerfile), 144 dockerfile2llb.WithInternalName(name), 145 ) 146 147 fileop := useFileOp(opts, &caps) 148 149 var buildContext *llb.State 150 isNotLocalContext := false 151 if st, ok := detectGitContext(opts[localNameContext], opts[keyContextKeepGitDir]); ok { 152 if !forceLocalDockerfile { 153 src = *st 154 } 155 buildContext = st 156 } else if httpPrefix.MatchString(opts[localNameContext]) { 157 httpContext := llb.HTTP(opts[localNameContext], llb.Filename("context"), dockerfile2llb.WithInternalName("load remote build context")) 158 def, err := httpContext.Marshal(ctx, marshalOpts...) 159 if err != nil { 160 return nil, errors.Wrapf(err, "failed to marshal httpcontext") 161 } 162 res, err := c.Solve(ctx, client.SolveRequest{ 163 Definition: def.ToPB(), 164 }) 165 if err != nil { 166 return nil, errors.Wrapf(err, "failed to resolve httpcontext") 167 } 168 169 ref, err := res.SingleRef() 170 if err != nil { 171 return nil, err 172 } 173 174 dt, err := ref.ReadFile(ctx, client.ReadRequest{ 175 Filename: "context", 176 Range: &client.FileRange{ 177 Length: 1024, 178 }, 179 }) 180 if err != nil { 181 return nil, errors.Wrapf(err, "failed to read downloaded context") 182 } 183 if isArchive(dt) { 184 if fileop { 185 bc := llb.Scratch().File(llb.Copy(httpContext, "/context", "/", &llb.CopyInfo{ 186 AttemptUnpack: true, 187 })) 188 if !forceLocalDockerfile { 189 src = bc 190 } 191 buildContext = &bc 192 } else { 193 copyImage := opts[keyOverrideCopyImage] 194 if copyImage == "" { 195 copyImage = dockerfile2llb.DefaultCopyImage 196 } 197 unpack := llb.Image(copyImage, dockerfile2llb.WithInternalName("helper image for file operations")). 198 Run(llb.Shlex("copy --unpack /src/context /out/"), llb.ReadonlyRootFS(), dockerfile2llb.WithInternalName("extracting build context")) 199 unpack.AddMount("/src", httpContext, llb.Readonly) 200 bc := unpack.AddMount("/out", llb.Scratch()) 201 if !forceLocalDockerfile { 202 src = bc 203 } 204 buildContext = &bc 205 } 206 } else { 207 filename = "context" 208 if !forceLocalDockerfile { 209 src = httpContext 210 } 211 buildContext = &httpContext 212 isNotLocalContext = true 213 } 214 } else if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil { 215 inputs, err := c.Inputs(ctx) 216 if err != nil { 217 return nil, errors.Wrapf(err, "failed to get frontend inputs") 218 } 219 220 if !forceLocalDockerfile { 221 inputDockerfile, ok := inputs[DefaultLocalNameDockerfile] 222 if ok { 223 src = inputDockerfile 224 } 225 } 226 227 inputCtx, ok := inputs[DefaultLocalNameContext] 228 if ok { 229 buildContext = &inputCtx 230 isNotLocalContext = true 231 } 232 } 233 234 if buildContext != nil { 235 if sub, ok := opts[keyContextSubDir]; ok { 236 buildContext = scopeToSubDir(buildContext, fileop, sub) 237 } 238 } 239 240 def, err := src.Marshal(ctx, marshalOpts...) 241 if err != nil { 242 return nil, errors.Wrapf(err, "failed to marshal local source") 243 } 244 245 var sourceMap *llb.SourceMap 246 247 eg, ctx2 := errgroup.WithContext(ctx) 248 var dtDockerfile []byte 249 var dtDockerignore []byte 250 var dtDockerignoreDefault []byte 251 eg.Go(func() error { 252 res, err := c.Solve(ctx2, client.SolveRequest{ 253 Definition: def.ToPB(), 254 }) 255 if err != nil { 256 return errors.Wrapf(err, "failed to resolve dockerfile") 257 } 258 259 ref, err := res.SingleRef() 260 if err != nil { 261 return err 262 } 263 264 dtDockerfile, err = ref.ReadFile(ctx2, client.ReadRequest{ 265 Filename: filename, 266 }) 267 if err != nil { 268 fallback := false 269 if path.Base(filename) == defaultDockerfileName { 270 var err1 error 271 dtDockerfile, err1 = ref.ReadFile(ctx2, client.ReadRequest{ 272 Filename: path.Join(path.Dir(filename), strings.ToLower(defaultDockerfileName)), 273 }) 274 if err1 == nil { 275 fallback = true 276 } 277 } 278 if !fallback { 279 return errors.Wrapf(err, "failed to read dockerfile") 280 } 281 } 282 283 sourceMap = llb.NewSourceMap(&src, filename, dtDockerfile) 284 sourceMap.Definition = def 285 286 dt, err := ref.ReadFile(ctx2, client.ReadRequest{ 287 Filename: filename + ".dockerignore", 288 }) 289 if err == nil { 290 dtDockerignore = dt 291 } 292 return nil 293 }) 294 var excludes []string 295 if !isNotLocalContext { 296 eg.Go(func() error { 297 dockerignoreState := buildContext 298 if dockerignoreState == nil { 299 st := llb.Local(localNameContext, 300 llb.SessionID(c.BuildOpts().SessionID), 301 llb.FollowPaths([]string{dockerignoreFilename}), 302 llb.SharedKeyHint(localNameContext+"-"+dockerignoreFilename), 303 dockerfile2llb.WithInternalName("load "+dockerignoreFilename), 304 ) 305 dockerignoreState = &st 306 } 307 def, err := dockerignoreState.Marshal(ctx, marshalOpts...) 308 if err != nil { 309 return err 310 } 311 res, err := c.Solve(ctx2, client.SolveRequest{ 312 Definition: def.ToPB(), 313 }) 314 if err != nil { 315 return err 316 } 317 ref, err := res.SingleRef() 318 if err != nil { 319 return err 320 } 321 dtDockerignoreDefault, err = ref.ReadFile(ctx2, client.ReadRequest{ 322 Filename: dockerignoreFilename, 323 }) 324 if err != nil { 325 return nil 326 } 327 return nil 328 }) 329 } 330 331 if err := eg.Wait(); err != nil { 332 return nil, err 333 } 334 335 if dtDockerignore == nil { 336 dtDockerignore = dtDockerignoreDefault 337 } 338 if dtDockerignore != nil { 339 excludes, err = dockerignore.ReadAll(bytes.NewBuffer(dtDockerignore)) 340 if err != nil { 341 return nil, errors.Wrap(err, "failed to parse dockerignore") 342 } 343 } 344 345 if _, ok := opts["cmdline"]; !ok { 346 if cmdline, ok := opts[keySyntax]; ok { 347 p := strings.SplitN(strings.TrimSpace(cmdline), " ", 2) 348 res, err := forwardGateway(ctx, c, p[0], cmdline) 349 if err != nil && len(errdefs.Sources(err)) == 0 { 350 return nil, errors.Wrapf(err, "failed with %s = %s", keySyntax, cmdline) 351 } 352 return res, err 353 } else if ref, cmdline, loc, ok := dockerfile2llb.DetectSyntax(bytes.NewBuffer(dtDockerfile)); ok { 354 res, err := forwardGateway(ctx, c, ref, cmdline) 355 if err != nil && len(errdefs.Sources(err)) == 0 { 356 return nil, wrapSource(err, sourceMap, loc) 357 } 358 return res, err 359 } 360 } 361 362 if capsError != nil { 363 return nil, capsError 364 } 365 366 if res, ok, err := checkSubRequest(ctx, opts); ok { 367 return res, err 368 } 369 370 exportMap := len(targetPlatforms) > 1 371 372 if v := opts[keyMultiPlatform]; v != "" { 373 b, err := strconv.ParseBool(v) 374 if err != nil { 375 return nil, errors.Errorf("invalid boolean value %s", v) 376 } 377 if !b && exportMap { 378 return nil, errors.Errorf("returning multiple target plaforms is not allowed") 379 } 380 exportMap = b 381 } 382 383 expPlatforms := &exptypes.Platforms{ 384 Platforms: make([]exptypes.Platform, len(targetPlatforms)), 385 } 386 res := client.NewResult() 387 388 eg, ctx = errgroup.WithContext(ctx) 389 390 for i, tp := range targetPlatforms { 391 func(i int, tp *specs.Platform) { 392 eg.Go(func() (err error) { 393 defer func() { 394 var el *parser.ErrorLocation 395 if errors.As(err, &el) { 396 err = wrapSource(err, sourceMap, el.Location) 397 } 398 }() 399 st, img, err := dockerfile2llb.Dockerfile2LLB(ctx, dtDockerfile, dockerfile2llb.ConvertOpt{ 400 Target: opts[keyTarget], 401 MetaResolver: c, 402 BuildArgs: filter(opts, buildArgPrefix), 403 Labels: filter(opts, labelPrefix), 404 CacheIDNamespace: opts[keyCacheNS], 405 SessionID: c.BuildOpts().SessionID, 406 BuildContext: buildContext, 407 Excludes: excludes, 408 IgnoreCache: ignoreCache, 409 TargetPlatform: tp, 410 BuildPlatforms: buildPlatforms, 411 ImageResolveMode: resolveMode, 412 PrefixPlatform: exportMap, 413 ExtraHosts: extraHosts, 414 ForceNetMode: defaultNetMode, 415 OverrideCopyImage: opts[keyOverrideCopyImage], 416 LLBCaps: &caps, 417 SourceMap: sourceMap, 418 Hostname: opts[keyHostname], 419 }) 420 421 if err != nil { 422 return errors.Wrapf(err, "failed to create LLB definition") 423 } 424 425 def, err := st.Marshal(ctx) 426 if err != nil { 427 return errors.Wrapf(err, "failed to marshal LLB definition") 428 } 429 430 config, err := json.Marshal(img) 431 if err != nil { 432 return errors.Wrapf(err, "failed to marshal image config") 433 } 434 435 var cacheImports []client.CacheOptionsEntry 436 // new API 437 if cacheImportsStr := opts[keyCacheImports]; cacheImportsStr != "" { 438 var cacheImportsUM []controlapi.CacheOptionsEntry 439 if err := json.Unmarshal([]byte(cacheImportsStr), &cacheImportsUM); err != nil { 440 return errors.Wrapf(err, "failed to unmarshal %s (%q)", keyCacheImports, cacheImportsStr) 441 } 442 for _, um := range cacheImportsUM { 443 cacheImports = append(cacheImports, client.CacheOptionsEntry{Type: um.Type, Attrs: um.Attrs}) 444 } 445 } 446 // old API 447 if cacheFromStr := opts[keyCacheFrom]; cacheFromStr != "" { 448 cacheFrom := strings.Split(cacheFromStr, ",") 449 for _, s := range cacheFrom { 450 im := client.CacheOptionsEntry{ 451 Type: "registry", 452 Attrs: map[string]string{ 453 "ref": s, 454 }, 455 } 456 // FIXME(AkihiroSuda): skip append if already exists 457 cacheImports = append(cacheImports, im) 458 } 459 } 460 461 r, err := c.Solve(ctx, client.SolveRequest{ 462 Definition: def.ToPB(), 463 CacheImports: cacheImports, 464 }) 465 if err != nil { 466 return err 467 } 468 469 ref, err := r.SingleRef() 470 if err != nil { 471 return err 472 } 473 474 if !exportMap { 475 res.AddMeta(exptypes.ExporterImageConfigKey, config) 476 res.SetRef(ref) 477 } else { 478 p := platforms.DefaultSpec() 479 if tp != nil { 480 p = *tp 481 } 482 483 k := platforms.Format(p) 484 res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, k), config) 485 res.AddRef(k, ref) 486 expPlatforms.Platforms[i] = exptypes.Platform{ 487 ID: k, 488 Platform: p, 489 } 490 } 491 return nil 492 }) 493 }(i, tp) 494 } 495 496 if err := eg.Wait(); err != nil { 497 return nil, err 498 } 499 500 if exportMap { 501 dt, err := json.Marshal(expPlatforms) 502 if err != nil { 503 return nil, err 504 } 505 res.AddMeta(exptypes.ExporterPlatformsKey, dt) 506 } 507 508 return res, nil 509} 510 511func forwardGateway(ctx context.Context, c client.Client, ref string, cmdline string) (*client.Result, error) { 512 opts := c.BuildOpts().Opts 513 if opts == nil { 514 opts = map[string]string{} 515 } 516 opts["cmdline"] = cmdline 517 opts["source"] = ref 518 519 gwcaps := c.BuildOpts().Caps 520 var frontendInputs map[string]*pb.Definition 521 if (&gwcaps).Supports(gwpb.CapFrontendInputs) == nil { 522 inputs, err := c.Inputs(ctx) 523 if err != nil { 524 return nil, errors.Wrapf(err, "failed to get frontend inputs") 525 } 526 527 frontendInputs = make(map[string]*pb.Definition) 528 for name, state := range inputs { 529 def, err := state.Marshal(ctx) 530 if err != nil { 531 return nil, err 532 } 533 frontendInputs[name] = def.ToPB() 534 } 535 } 536 537 return c.Solve(ctx, client.SolveRequest{ 538 Frontend: "gateway.v0", 539 FrontendOpt: opts, 540 FrontendInputs: frontendInputs, 541 }) 542} 543 544func filter(opt map[string]string, key string) map[string]string { 545 m := map[string]string{} 546 for k, v := range opt { 547 if strings.HasPrefix(k, key) { 548 m[strings.TrimPrefix(k, key)] = v 549 } 550 } 551 return m 552} 553 554func detectGitContext(ref, gitContext string) (*llb.State, bool) { 555 found := false 556 if httpPrefix.MatchString(ref) && gitURLPathWithFragmentSuffix.MatchString(ref) { 557 found = true 558 } 559 560 keepGit := false 561 if gitContext != "" { 562 if v, err := strconv.ParseBool(gitContext); err == nil { 563 keepGit = v 564 } 565 } 566 567 for _, prefix := range []string{"git://", "github.com/", "git@"} { 568 if strings.HasPrefix(ref, prefix) { 569 found = true 570 break 571 } 572 } 573 if !found { 574 return nil, false 575 } 576 577 parts := strings.SplitN(ref, "#", 2) 578 branch := "" 579 if len(parts) > 1 { 580 branch = parts[1] 581 } 582 gitOpts := []llb.GitOption{dockerfile2llb.WithInternalName("load git source " + ref)} 583 if keepGit { 584 gitOpts = append(gitOpts, llb.KeepGitDir()) 585 } 586 587 st := llb.Git(parts[0], branch, gitOpts...) 588 return &st, true 589} 590 591func isArchive(header []byte) bool { 592 for _, m := range [][]byte{ 593 {0x42, 0x5A, 0x68}, // bzip2 594 {0x1F, 0x8B, 0x08}, // gzip 595 {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}, // xz 596 } { 597 if len(header) < len(m) { 598 continue 599 } 600 if bytes.Equal(m, header[:len(m)]) { 601 return true 602 } 603 } 604 605 r := tar.NewReader(bytes.NewBuffer(header)) 606 _, err := r.Next() 607 return err == nil 608} 609 610func parsePlatforms(v string) ([]*specs.Platform, error) { 611 var pp []*specs.Platform 612 for _, v := range strings.Split(v, ",") { 613 p, err := platforms.Parse(v) 614 if err != nil { 615 return nil, errors.Wrapf(err, "failed to parse target platform %s", v) 616 } 617 p = platforms.Normalize(p) 618 pp = append(pp, &p) 619 } 620 return pp, nil 621} 622 623func parseResolveMode(v string) (llb.ResolveMode, error) { 624 switch v { 625 case pb.AttrImageResolveModeDefault, "": 626 return llb.ResolveModeDefault, nil 627 case pb.AttrImageResolveModeForcePull: 628 return llb.ResolveModeForcePull, nil 629 case pb.AttrImageResolveModePreferLocal: 630 return llb.ResolveModePreferLocal, nil 631 default: 632 return 0, errors.Errorf("invalid image-resolve-mode: %s", v) 633 } 634} 635 636func parseExtraHosts(v string) ([]llb.HostIP, error) { 637 if v == "" { 638 return nil, nil 639 } 640 out := make([]llb.HostIP, 0) 641 csvReader := csv.NewReader(strings.NewReader(v)) 642 fields, err := csvReader.Read() 643 if err != nil { 644 return nil, err 645 } 646 for _, field := range fields { 647 parts := strings.SplitN(field, "=", 2) 648 if len(parts) != 2 { 649 return nil, errors.Errorf("invalid key-value pair %s", field) 650 } 651 key := strings.ToLower(parts[0]) 652 val := strings.ToLower(parts[1]) 653 ip := net.ParseIP(val) 654 if ip == nil { 655 return nil, errors.Errorf("failed to parse IP %s", val) 656 } 657 out = append(out, llb.HostIP{Host: key, IP: ip}) 658 } 659 return out, nil 660} 661 662func parseNetMode(v string) (pb.NetMode, error) { 663 if v == "" { 664 return llb.NetModeSandbox, nil 665 } 666 switch v { 667 case "none": 668 return llb.NetModeNone, nil 669 case "host": 670 return llb.NetModeHost, nil 671 case "sandbox": 672 return llb.NetModeSandbox, nil 673 default: 674 return 0, errors.Errorf("invalid netmode %s", v) 675 } 676} 677 678func useFileOp(args map[string]string, caps *apicaps.CapSet) bool { 679 enabled := true 680 if v, ok := args["build-arg:BUILDKIT_DISABLE_FILEOP"]; ok { 681 if b, err := strconv.ParseBool(v); err == nil { 682 enabled = !b 683 } 684 } 685 return enabled && caps != nil && caps.Supports(pb.CapFileBase) == nil 686} 687 688func scopeToSubDir(c *llb.State, fileop bool, dir string) *llb.State { 689 if fileop { 690 bc := llb.Scratch().File(llb.Copy(*c, dir, "/", &llb.CopyInfo{ 691 CopyDirContentsOnly: true, 692 })) 693 return &bc 694 } 695 unpack := llb.Image(dockerfile2llb.DefaultCopyImage, dockerfile2llb.WithInternalName("helper image for file operations")). 696 Run(llb.Shlexf("copy %s/. /out/", path.Join("/src", dir)), llb.ReadonlyRootFS(), dockerfile2llb.WithInternalName("filtering build context")) 697 unpack.AddMount("/src", *c, llb.Readonly) 698 bc := unpack.AddMount("/out", llb.Scratch()) 699 return &bc 700} 701 702func wrapSource(err error, sm *llb.SourceMap, ranges []parser.Range) error { 703 if sm == nil { 704 return err 705 } 706 s := errdefs.Source{ 707 Info: &pb.SourceInfo{ 708 Data: sm.Data, 709 Filename: sm.Filename, 710 Definition: sm.Definition.ToPB(), 711 }, 712 Ranges: make([]*pb.Range, 0, len(ranges)), 713 } 714 for _, r := range ranges { 715 s.Ranges = append(s.Ranges, &pb.Range{ 716 Start: pb.Position{ 717 Line: int32(r.Start.Line), 718 Character: int32(r.Start.Character), 719 }, 720 End: pb.Position{ 721 Line: int32(r.End.Line), 722 Character: int32(r.End.Character), 723 }, 724 }) 725 } 726 return errdefs.WithSource(err, s) 727} 728