1
1
/*
2
- Copyright 2019 The Kubernetes Authors.
2
+ Copyright 2019-2022 The Kubernetes Authors.
3
3
4
4
Licensed under the Apache License, Version 2.0 (the "License");
5
5
you may not use this file except in compliance with the License.
@@ -25,9 +25,12 @@ import (
25
25
"go/types"
26
26
"io/ioutil"
27
27
"os"
28
+ "path/filepath"
29
+ "regexp"
28
30
"sync"
29
31
30
32
"golang.org/x/tools/go/packages"
33
+ "k8s.io/apimachinery/pkg/util/sets"
31
34
)
32
35
33
36
// Much of this is strongly inspired by the contents of go/packages,
@@ -329,6 +332,40 @@ func LoadRoots(roots ...string) ([]*Package, error) {
329
332
//
330
333
// This is generally only useful for use in testing when you need to modify
331
334
// loading settings to load from a fake location.
335
+ //
336
+ // This function will traverse Go module boundaries for roots that are file-
337
+ // system paths and end with "...". Please note this feature currently only
338
+ // supports roots that are filesystem paths. For more information, please
339
+ // refer to the high-level outline of this function's logic:
340
+ //
341
+ // 1. If no roots are provided then load the working directory and return
342
+ // early.
343
+ //
344
+ // 2. Otherwise sort the provided roots into two, distinct buckets:
345
+ //
346
+ // a. package/module names
347
+ // b. filesystem paths
348
+ //
349
+ // A filesystem path is distinguished from a Go package/module name by
350
+ // the same rules as followed by the "go" command. At a high level, a
351
+ // root is a filesystem path IFF it meets ANY of the following criteria:
352
+ //
353
+ // * is absolute
354
+ // * begins with .
355
+ // * begins with ..
356
+ //
357
+ // For more information please refer to the output of the command
358
+ // "go help packages".
359
+ //
360
+ // 3. Load the package/module roots as a single call to packages.Load. If
361
+ // there are no filesystem path roots then return early.
362
+ //
363
+ // 4. For filesystem path roots ending with "...", check to see if its
364
+ // descendants include any nested, Go modules. If so, add the directory
365
+ // that contains the nested Go module to the filesystem path roots.
366
+ //
367
+ // 5. Load the filesystem path roots and return the load packages for the
368
+ // package/module roots AND the filesystem path roots.
332
369
func LoadRootsWithConfig (cfg * packages.Config , roots ... string ) ([]* Package , error ) {
333
370
l := & loader {
334
371
cfg : cfg ,
@@ -341,13 +378,208 @@ func LoadRootsWithConfig(cfg *packages.Config, roots ...string) ([]*Package, err
341
378
// put our build flags first so that callers can override them
342
379
l .cfg .BuildFlags = append ([]string {"-tags" , "ignore_autogenerated" }, l .cfg .BuildFlags ... )
343
380
344
- rawPkgs , err := packages .Load (l .cfg , roots ... )
345
- if err != nil {
346
- return nil , err
381
+ // uniquePkgIDs is used to keep track of the discovered packages to be nice
382
+ // and try and prevent packages from showing up twice when nested module
383
+ // support is enabled. there is not harm that comes from this per se, but
384
+ // it makes testing easier when a known number of modules can be asserted
385
+ uniquePkgIDs := sets.String {}
386
+
387
+ // loadPackages returns the Go packages for the provided roots
388
+ //
389
+ // if validatePkgFn is nil, a package will be returned in the slice,
390
+ // otherwise the package is only returned if the result of
391
+ // validatePkgFn(pkg.ID) is truthy
392
+ loadPackages := func (roots ... string ) ([]* Package , error ) {
393
+ rawPkgs , err := packages .Load (l .cfg , roots ... )
394
+ if err != nil {
395
+ return nil , err
396
+ }
397
+ var pkgs []* Package
398
+ for _ , rp := range rawPkgs {
399
+ p := l .packageFor (rp )
400
+ if ! uniquePkgIDs .Has (p .ID ) {
401
+ pkgs = append (pkgs , p )
402
+ uniquePkgIDs .Insert (p .ID )
403
+ }
404
+ }
405
+ return pkgs , nil
406
+ }
407
+
408
+ // if no roots were provided then load the current package and return early
409
+ if len (roots ) == 0 {
410
+ pkgs , err := loadPackages ()
411
+ if err != nil {
412
+ return nil , err
413
+ }
414
+ l .Roots = append (l .Roots , pkgs ... )
415
+ return l .Roots , nil
416
+ }
417
+
418
+ // pkgRoots is a slice of roots that are package/modules and fspRoots
419
+ // is a slice of roots that are local filesystem paths.
420
+ //
421
+ // please refer to this function's godoc comments for more information on
422
+ // how these two types of roots are distinguished from one another
423
+ var (
424
+ pkgRoots []string
425
+ fspRoots []string
426
+ fspRootRx = regexp .MustCompile (`^\.{1,2}` )
427
+ )
428
+ for _ , r := range roots {
429
+ if filepath .IsAbs (r ) || fspRootRx .MatchString (r ) {
430
+ fspRoots = append (fspRoots , r )
431
+ } else {
432
+ pkgRoots = append (pkgRoots , r )
433
+ }
434
+ }
435
+
436
+ // handle the package roots by sending them into the packages.Load function
437
+ // all at once. this is more efficient, but cannot be used for the file-
438
+ // system path roots due to them needing a custom, calculated value for the
439
+ // cfg.Dir field
440
+ if len (pkgRoots ) > 0 {
441
+ pkgs , err := loadPackages (pkgRoots ... )
442
+ if err != nil {
443
+ return nil , err
444
+ }
445
+ l .Roots = append (l .Roots , pkgs ... )
446
+ }
447
+
448
+ // if there are no filesystem path roots then go ahead and return early
449
+ if len (fspRoots ) == 0 {
450
+ return l .Roots , nil
451
+ }
452
+
453
+ //
454
+ // at this point we are handling filesystem path roots
455
+ //
456
+
457
+ // ensure the cfg.Dir field is reset to its original value upon
458
+ // returning from this function. it should honestly be fine if it is
459
+ // not given most callers will not send in the cfg parameter directly,
460
+ // as it's largely for testing, but still, let's be good stewards.
461
+ defer func (d string ) {
462
+ cfg .Dir = d
463
+ }(cfg .Dir )
464
+
465
+ // store the value of cfg.Dir so we can use it later if it is non-empty.
466
+ // we need to store it now as the value of cfg.Dir will be updated by
467
+ // a loop below
468
+ cfgDir := cfg .Dir
469
+
470
+ // addNestedGoModulesToRoots is given to filepath.WalkDir and adds the
471
+ // directory part of p to the list of filesystem path roots IFF p is the
472
+ // path to a file named "go.mod"
473
+ addNestedGoModulesToRoots := func (
474
+ p string ,
475
+ d os.DirEntry ,
476
+ e error ) error {
477
+
478
+ if e != nil {
479
+ return e
480
+ }
481
+ if ! d .IsDir () && filepath .Base (p ) == "go.mod" {
482
+ fspRoots = append (fspRoots , filepath .Join (filepath .Dir (p ), "..." ))
483
+ }
484
+ return nil
347
485
}
348
486
349
- for _ , rawPkg := range rawPkgs {
350
- l .Roots = append (l .Roots , l .packageFor (rawPkg ))
487
+ // in the first pass over the filesystem path roots we:
488
+ //
489
+ // 1. make the root into an absolute path
490
+ //
491
+ // 2. check to see if a root uses the nested path syntax, ex. ...
492
+ //
493
+ // 3. if so, walk the root's descendants, searching for any nested Go
494
+ // modules
495
+ //
496
+ // 4. if found then the directory containing the Go module is added to
497
+ // the list of the filesystem path roots
498
+ for i := range fspRoots {
499
+ r := fspRoots [i ]
500
+
501
+ // clean up the root
502
+ r = filepath .Clean (r )
503
+
504
+ // get the absolute path of the root
505
+ if ! filepath .IsAbs (r ) {
506
+
507
+ // if the initial value of cfg.Dir was non-empty then use it when
508
+ // building the absolute path to this root. otherwise use the
509
+ // filepath.Abs function to get the absolute path of the root based
510
+ // on the working directory
511
+ if cfgDir != "" {
512
+ r = filepath .Join (cfgDir , r )
513
+ } else {
514
+ ar , err := filepath .Abs (r )
515
+ if err != nil {
516
+ return nil , err
517
+ }
518
+ r = ar
519
+ }
520
+ }
521
+
522
+ // update the root to be an absolute path
523
+ fspRoots [i ] = r
524
+
525
+ b , d := filepath .Base (r ), filepath .Dir (r )
526
+
527
+ // if the base element is "..." then it means nested traversal is
528
+ // activated. this can be passed directly to the loader. however, if
529
+ // specified we also want to traverse the path manually to determine if
530
+ // there are any nested Go modules we want to add to the list of file-
531
+ // system path roots to process
532
+ if b == "..." {
533
+ if err := filepath .WalkDir (
534
+ d ,
535
+ addNestedGoModulesToRoots ); err != nil {
536
+
537
+ return nil , err
538
+ }
539
+ }
540
+ }
541
+
542
+ // in the second pass over the filesystem path roots we:
543
+ //
544
+ // 1. determine the directory from which to execute the loader
545
+ //
546
+ // 2. update the loader config's Dir property to be the directory from
547
+ // step one
548
+ //
549
+ // 3. determine whether the root passed to the loader should be "./."
550
+ // or "./..."
551
+ //
552
+ // 4. execute the loader with the value from step three
553
+ for _ , r := range fspRoots {
554
+ b , d := filepath .Base (r ), filepath .Dir (r )
555
+
556
+ // we want the base part of the path to be either "..." or ".", except
557
+ // Go's filepath utilities clean paths during manipulation, removing the
558
+ // ".". thus, if not "...", let's update the path components so that:
559
+ //
560
+ // d = r
561
+ // b = "."
562
+ if b != "..." {
563
+ d = r
564
+ b = "."
565
+ }
566
+
567
+ // update the loader configuration's Dir field to the directory part of
568
+ // the root
569
+ l .cfg .Dir = d
570
+
571
+ // update the root to be "./..." or "./."
572
+ // (with OS-specific filepath separator). please note filepath.Join
573
+ // would clean up the trailing "." character that we want preserved,
574
+ // hence the more manual path concatenation logic
575
+ r = fmt .Sprintf (".%s%s" , string (filepath .Separator ), b )
576
+
577
+ // load the packages from the roots
578
+ pkgs , err := loadPackages (r )
579
+ if err != nil {
580
+ return nil , err
581
+ }
582
+ l .Roots = append (l .Roots , pkgs ... )
351
583
}
352
584
353
585
return l .Roots , nil
0 commit comments