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
+ "strings"
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,7 +332,7 @@ 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.
332
- func LoadRootsWithConfig (cfg * packages.Config , roots ... string ) ([]* Package , error ) {
335
+ func LoadRootsWithConfig (cfg * packages.Config , roots ... string ) (pkgs []* Package , retErr error ) {
333
336
l := & loader {
334
337
cfg : cfg ,
335
338
packages : make (map [* packages.Package ]* Package ),
@@ -341,13 +344,221 @@ func LoadRootsWithConfig(cfg *packages.Config, roots ...string) ([]*Package, err
341
344
// put our build flags first so that callers can override them
342
345
l .cfg .BuildFlags = append ([]string {"-tags" , "ignore_autogenerated" }, l .cfg .BuildFlags ... )
343
346
344
- rawPkgs , err := packages .Load (l .cfg , roots ... )
345
- if err != nil {
346
- return nil , err
347
+ // ensure the cfg.Dir field is reset to its original value upon
348
+ // returning from this function. it should honestly be fine if it is
349
+ // not given most callers will not send in the cfg parameter directly,
350
+ // as it's largely for testing, but still, let's be good stewards.
351
+ defer func (d string ) {
352
+ cfg .Dir = d
353
+ }(cfg .Dir )
354
+
355
+ // ensure cfg.Dir is absolute. it just makes the rest of the code easier
356
+ // to debug when there are any issues
357
+ if cfg .Dir != "" && ! filepath .IsAbs (cfg .Dir ) {
358
+ ap , err := filepath .Abs (cfg .Dir )
359
+ if err != nil {
360
+ return nil , err
361
+ }
362
+ cfg .Dir = ap
347
363
}
348
364
349
- for _ , rawPkg := range rawPkgs {
350
- l .Roots = append (l .Roots , l .packageFor (rawPkg ))
365
+ // loadRoots is a helper function used in three locations below.
366
+ loadRoots := func (uniqueRoots * sets.String , roots ... string ) error {
367
+ // load the root and gets its raw packages
368
+ rawPkgs , err := packages .Load (l .cfg , roots ... )
369
+ if err != nil {
370
+ return err
371
+ }
372
+
373
+ // get the package path for each raw package
374
+ for _ , rawPkg := range rawPkgs {
375
+ pkg := l .packageFor (rawPkg )
376
+
377
+ // if a set was provided to prevent duplicate packages, use it,
378
+ // otherwise append to the package list
379
+ if uniqueRoots == nil {
380
+ l .Roots = append (l .Roots , pkg )
381
+ } else if ! uniqueRoots .Has (pkg .ID ) {
382
+ l .Roots = append (l .Roots , pkg )
383
+ uniqueRoots .Insert (pkg .ID )
384
+ }
385
+ }
386
+
387
+ return nil
388
+ }
389
+
390
+ // if no roots were provided then load the current package and return early
391
+ if len (roots ) == 0 {
392
+ if err := loadRoots (nil ); err != nil {
393
+ return nil , err
394
+ }
395
+ return l .Roots , nil
396
+ }
397
+
398
+ // store the value of cfg.Dir so we can use it later if it is non-empty.
399
+ // we need to store it now as the value of cfg.Dir will be updated by
400
+ // a loop below
401
+ cfgDir := cfg .Dir
402
+
403
+ // uniqueRoots is used to keep track of the discovered packages to be nice
404
+ // and try and prevent packages from showing up twice when nested module
405
+ // support is enabled. there is not harm that comes from this per se, but
406
+ // it makes testing easier when a known number of modules can be asserted
407
+ uniqueRoots := sets.String {}
408
+
409
+ // isFSRoots is used to keep track of whether a root is a filesystem path
410
+ // or a package/module name. the logic to determine this is based on the
411
+ // output from "go help packages". The "go" command determines if a path
412
+ // is a filesystem path based on whether it is:
413
+ //
414
+ // * absolute
415
+ // * begins with "."
416
+ // * begins with ".."
417
+ //
418
+ // otherwise the path is considered to be a package. this set is a list of
419
+ // the indices from the roots slice that are filesystem paths
420
+ isFSRoots := sets.Int {}
421
+
422
+ // addNestedGoModulesToRoots is given to filepath.WalkDir
423
+ // and adds the directory part of p to the list of roots
424
+ // IFF p is the path to a file named "go.mod"
425
+ addNestedGoModulesToRoots := func (
426
+ p string ,
427
+ d os.DirEntry ,
428
+ e error ) error {
429
+
430
+ if e != nil {
431
+ return e
432
+ }
433
+ if ! d .IsDir () && filepath .Base (p ) == "go.mod" {
434
+ roots = append (roots , filepath .Join (filepath .Dir (p ), "..." ))
435
+ isFSRoots .Insert (len (roots ) - 1 )
436
+ }
437
+ return nil
438
+ }
439
+
440
+ // the basic loader logic is as follows:
441
+ //
442
+ // 1. iterate over the list of provided roots
443
+ //
444
+ // 2. if a root uses the nested path syntax, ex. ..., then walk the
445
+ // root's descendants to search for any any nested Go modules, and if
446
+ // found, load them to the list of roots
447
+ //
448
+ // 3. iterate over the list of updated roots
449
+ //
450
+ // 4. update the loader config's Dir property to be the directory element
451
+ // of the current root
452
+ //
453
+ // 5. execute the loader on the base element of the current root, which
454
+ // will be either "./." or "./..."
455
+ //
456
+ // the following range operation is parts 1-2
457
+ for i := range roots {
458
+ r := roots [i ]
459
+
460
+ //fmt.Printf("1.processing root=%s\n", r)
461
+
462
+ // based on the logic from "go help packages", a path is a package/mod
463
+ // name if it is absolute or begins with "." or "..". If it is none of
464
+ // those things then go ahead and skip to the next iteration of the loop
465
+ if ! filepath .IsAbs (r ) &&
466
+ ! strings .HasPrefix (r , "." ) &&
467
+ ! strings .HasPrefix (r , ".." ) {
468
+
469
+ continue
470
+ }
471
+
472
+ //fmt.Printf("1.processing root as file=%s\n", r)
473
+
474
+ // this is a filesytem path
475
+ isFSRoots .Insert (i )
476
+
477
+ // clean up the root
478
+ r = filepath .Clean (r )
479
+
480
+ // get the absolute path of the root
481
+ if ! filepath .IsAbs (r ) {
482
+
483
+ // if the initial value of cfg.Dir was non-empty then use it when
484
+ // building the absolute path to this root. otherwise use the
485
+ // filepath.Abs function to get the absolute path of the root based
486
+ // on the working directory
487
+ if cfgDir != "" {
488
+ r = filepath .Join (cfgDir , r )
489
+ } else {
490
+ ar , err := filepath .Abs (r )
491
+ if err != nil {
492
+ return nil , err
493
+ }
494
+ r = ar
495
+ }
496
+ }
497
+
498
+ // update the root to be an absolute path
499
+ roots [i ] = r
500
+
501
+ b , d := filepath .Base (r ), filepath .Dir (r )
502
+
503
+ // if the base element is "..." then it means nested traversal is
504
+ // activated. this can be passed directly to the loader. however, if
505
+ // specified we also want to traverse the path manually to determine if
506
+ // there are any nested Go modules we want to add to the list of roots
507
+ // to process
508
+ if b == "..." {
509
+ if err := filepath .WalkDir (
510
+ d ,
511
+ addNestedGoModulesToRoots ); err != nil {
512
+
513
+ return nil , err
514
+ }
515
+ }
516
+ }
517
+
518
+ // this range operation is parts 3-5 from above.
519
+ for i := range roots {
520
+ r := roots [i ]
521
+
522
+ //fmt.Printf("2.processing root=%s\n", r)
523
+
524
+ // if the root is not a filesystem path then just load it directly
525
+ // and skip to the next iteration of the loop
526
+ if ! isFSRoots .Has (i ) {
527
+ l .cfg .Dir = cfgDir
528
+ if err := loadRoots (& uniqueRoots , r ); err != nil {
529
+ return nil , err
530
+ }
531
+ //fmt.Printf("2.skipping root=%s\n", r)
532
+ continue
533
+ }
534
+
535
+ b , d := filepath .Base (r ), filepath .Dir (r )
536
+
537
+ // we want the base part of the path to be either "..." or ".", except
538
+ // Go's filepath utilities clean paths during manipulation, removing the
539
+ // ".". thus, if not "...", let's update the path components so that:
540
+ //
541
+ // d = r
542
+ // b = "."
543
+ if b != "..." {
544
+ d = r
545
+ b = "."
546
+ }
547
+
548
+ // update the loader configuration's Dir field to the directory part of
549
+ // the root
550
+ l .cfg .Dir = d
551
+
552
+ // update the root to be "./..." or "./."
553
+ // (with OS-specific filepath separator). please note filepath.Join
554
+ // would clean up the trailing "." character that we want preserved,
555
+ // hence the more manual path concatenation logic
556
+ r = fmt .Sprintf (".%s%s" , string (filepath .Separator ), b )
557
+
558
+ // load the root
559
+ if err := loadRoots (& uniqueRoots , r ); err != nil {
560
+ return nil , err
561
+ }
351
562
}
352
563
353
564
return l .Roots , nil
0 commit comments