Skip to content

Commit 067a1ea

Browse files
committed
feat: allow 'graph' to be generated without 'target'
This switches off dependency resolution mode and dumps all the stages (targets) as a graph. While working on this feature, I discovered two cases when parts of the build graph were duplicated: first when packages were resolved, leading to duplicate nodes in the graph, second when LLB is generated. Buildkit seems to handle correctly LLB de-duplication, but anyway it's better to skip duplicate entries. Fixes #22 As examples, see siderolabs/tools#73 and siderolabs/pkgs#46 Signed-off-by: Andrey Smirnov <[email protected]>
1 parent cdc4af3 commit 067a1ea

File tree

6 files changed

+94
-24
lines changed

6 files changed

+94
-24
lines changed

cmd/graph.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,30 @@ starting from target to all the dependencies.
2424
Root: pkgRoot,
2525
Context: options.GetVariables(),
2626
}
27+
2728
packages, err := solver.NewPackages(&loader)
2829
if err != nil {
2930
log.Fatal(err)
3031
}
31-
graph, err := packages.Resolve(options.Target)
32-
if err != nil {
33-
log.Fatal(err)
32+
33+
var packageSet solver.PackageSet
34+
35+
if options.Target != "" {
36+
graph, err := packages.Resolve(options.Target)
37+
if err != nil {
38+
log.Fatal(err)
39+
}
40+
41+
packageSet = graph.ToSet()
42+
} else {
43+
packageSet = packages.ToSet()
3444
}
35-
graph.DumpDot(os.Stdout)
45+
46+
packageSet.DumpDot(os.Stdout)
3647
},
3748
}
3849

3950
func init() {
40-
graphCmd.Flags().StringVarP(&options.Target, "target", "t", "", "Target image to build")
41-
graphCmd.MarkFlagRequired("target") //nolint: errcheck
51+
graphCmd.Flags().StringVarP(&options.Target, "target", "t", "", "Target image to graph, if not set - graph all stages")
4252
rootCmd.AddCommand(graphCmd)
4353
}

internal/pkg/convert/graph.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@ type GraphLLB struct {
2525
BaseImages map[v1alpha2.Variant]llb.State
2626
Checksummer llb.State
2727
LocalContext llb.State
28+
29+
cache map[*solver.PackageNode]llb.State
2830
}
2931

3032
// NewGraphLLB creates new GraphLLB and initializes shared images.
3133
func NewGraphLLB(graph *solver.PackageGraph, options *environment.Options) *GraphLLB {
3234
result := &GraphLLB{
3335
PackageGraph: graph,
3436
Options: options,
37+
cache: make(map[*solver.PackageNode]llb.State),
3538
}
3639

3740
result.buildBaseImages()

internal/pkg/convert/node.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package convert
66

77
import (
88
"fmt"
9+
"log"
910
"path/filepath"
1011
"sort"
1112

@@ -214,6 +215,11 @@ func (node *NodeLLB) finalize(root llb.State) llb.State {
214215
func (node *NodeLLB) Build() (llb.State, error) {
215216
var err error
216217

218+
if state, ok := node.Graph.cache[node.PackageNode]; ok {
219+
log.Printf("cached node %s", node.Name)
220+
return state, nil
221+
}
222+
217223
root := node.base()
218224
root = node.install(root)
219225
root = node.context(root)
@@ -229,5 +235,7 @@ func (node *NodeLLB) Build() (llb.State, error) {
229235

230236
root = node.finalize(root)
231237

238+
node.Graph.cache[node.PackageNode] = root
239+
232240
return root, nil
233241
}

internal/pkg/solver/graph.go

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
package solver
66

77
import (
8-
"io"
9-
108
"github.com/emicklei/dot"
119

1210
"github.com/talos-systems/bldr/internal/pkg/types/v1alpha2"
@@ -23,11 +21,9 @@ type PackageNode struct {
2321
func (node *PackageNode) DumpDot(g *dot.Graph) dot.Node {
2422
n := g.Node(node.Name)
2523

26-
for _, dep := range node.DependsOn {
27-
depNode := dep.DumpDot(g)
28-
if len(depNode.EdgesTo(n)) == 0 {
29-
depNode.Edge(n)
30-
}
24+
for _, dep := range node.Pkg.InternalDependencies() {
25+
depNode := g.Node(dep)
26+
depNode.Edge(n)
3127
}
3228

3329
for _, dep := range node.Pkg.ExternalDependencies() {
@@ -36,9 +32,7 @@ func (node *PackageNode) DumpDot(g *dot.Graph) dot.Node {
3632
imageNode.Attr("fillcolor", "lemonchiffon")
3733
imageNode.Attr("style", "filled")
3834

39-
if len(imageNode.EdgesTo(n)) == 0 {
40-
imageNode.Edge(n)
41-
}
35+
imageNode.Edge(n)
4236
}
4337

4438
return n
@@ -49,10 +43,22 @@ type PackageGraph struct {
4943
Root *PackageNode
5044
}
5145

52-
// DumpDot dumps whole graph in dot format
53-
func (graph *PackageGraph) DumpDot(w io.Writer) {
54-
g := dot.NewGraph(dot.Directed)
55-
graph.Root.DumpDot(g)
46+
func (graph *PackageGraph) flatten(set PackageSet, node *PackageNode, skip map[*PackageNode]struct{}) PackageSet {
47+
if _, exists := skip[node]; exists {
48+
return set
49+
}
50+
51+
set = append(set, node)
52+
skip[node] = struct{}{}
53+
54+
for _, dep := range node.DependsOn {
55+
set = graph.flatten(set, dep, skip)
56+
}
57+
58+
return set
59+
}
5660

57-
g.Write(w)
61+
// ToSet converts graph to set of nodes
62+
func (graph *PackageGraph) ToSet() PackageSet {
63+
return graph.flatten(nil, graph.Root, make(map[*PackageNode]struct{}))
5864
}

internal/pkg/solver/packages.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ func NewPackages(loader PackageLoader) (*Packages, error) {
3939
return result, nil
4040
}
4141

42-
func (pkgs *Packages) resolve(name string, path []string) (*PackageNode, error) {
42+
func (pkgs *Packages) resolve(name string, path []string, cache map[string]*PackageNode) (*PackageNode, error) {
43+
if node := cache[name]; node != nil {
44+
return node, nil
45+
}
46+
4347
pkg := pkgs.packages[name]
4448
if pkg == nil {
4549
return nil, fmt.Errorf("package %q not defined", name)
@@ -60,23 +64,37 @@ func (pkgs *Packages) resolve(name string, path []string) (*PackageNode, error)
6064

6165
deps := pkg.InternalDependencies()
6266
for _, dep := range deps {
63-
depPkg, err := pkgs.resolve(dep, path)
67+
depPkg, err := pkgs.resolve(dep, path, cache)
6468
if err != nil {
6569
return nil, fmt.Errorf("error resolving dependency %q of %q: %w", dep, name, err)
6670
}
6771

6872
node.DependsOn = append(node.DependsOn, depPkg)
6973
}
7074

75+
cache[name] = node
76+
7177
return node, nil
7278
}
7379

7480
// Resolve trims down the package tree to have only deps of the target
7581
func (pkgs *Packages) Resolve(target string) (*PackageGraph, error) {
76-
root, err := pkgs.resolve(target, nil)
82+
root, err := pkgs.resolve(target, nil, make(map[string]*PackageNode))
7783
if err != nil {
7884
return nil, err
7985
}
8086

8187
return &PackageGraph{root}, nil
8288
}
89+
90+
// ToSet converts to set of package nodes
91+
func (pkgs *Packages) ToSet() (set PackageSet) {
92+
for name, pkg := range pkgs.packages {
93+
set = append(set, &PackageNode{
94+
Name: name,
95+
Pkg: pkg,
96+
})
97+
}
98+
99+
return
100+
}

internal/pkg/solver/set.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package solver
6+
7+
import (
8+
"io"
9+
10+
"github.com/emicklei/dot"
11+
)
12+
13+
// PackageSet is a list of PackageNodes
14+
type PackageSet []*PackageNode
15+
16+
// DumpDot dumps nodes and deps in dot format
17+
func (set PackageSet) DumpDot(w io.Writer) {
18+
g := dot.NewGraph(dot.Directed)
19+
20+
for _, node := range set {
21+
node.DumpDot(g)
22+
}
23+
24+
g.Write(w)
25+
}

0 commit comments

Comments
 (0)