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