Skip to content

Commit fdbd39e

Browse files
feat(wanda): Build dependencies in topological order
Extend Build() to automatically build all dependencies before building the root spec. In local mode, specs are discovered by scanning the repo for *.wanda.yaml files and built in topological order. For a slight optimization, in RayCI mode, only the root spec is built (deps built by prior pipeline steps). Topic: wanda-build-deps Relative: wanda-deps Signed-off-by: andrew <andrew@anyscale.com>
1 parent d437c29 commit fdbd39e

File tree

2 files changed

+92
-6
lines changed

2 files changed

+92
-6
lines changed

wanda/forge.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,46 @@ import (
1414
"github.com/google/go-containerregistry/pkg/v1/remote"
1515
)
1616

17-
// Build builds a container image from the given specification file.
17+
// Build builds a container image from the given specification file, and builds
18+
// all its dependencies in topological order.
19+
// In RayCI mode, dependencies are assumed built by prior pipeline steps; only
20+
// the root is built.
1821
func Build(specFile string, config *ForgeConfig) error {
1922
if config == nil {
2023
config = &ForgeConfig{}
2124
}
2225

23-
spec, err := parseSpecFile(specFile)
26+
graph, err := buildDepGraph(specFile, os.LookupEnv, config.NamePrefix)
2427
if err != nil {
25-
return fmt.Errorf("parse spec file: %w", err)
28+
return fmt.Errorf("build dep graph: %w", err)
2629
}
2730

28-
// Expand env variable.
29-
spec = spec.expandVar(os.LookupEnv)
31+
if err := graph.validateDeps(); err != nil {
32+
return fmt.Errorf("validate deps: %w", err)
33+
}
3034

3135
forge, err := NewForge(config)
3236
if err != nil {
3337
return fmt.Errorf("make forge: %w", err)
3438
}
35-
return forge.Build(spec)
39+
40+
// In RayCI mode, only build the root (deps built by prior pipeline steps).
41+
order := graph.Order
42+
if config.RayCI {
43+
order = []string{graph.Root}
44+
}
45+
46+
for _, name := range order {
47+
rs := graph.Specs[name]
48+
49+
log.Printf("building %s (from %s)", name, rs.Path)
50+
51+
if err := forge.Build(rs.Spec); err != nil {
52+
return fmt.Errorf("build %s: %w", name, err)
53+
}
54+
}
55+
56+
return nil
3657
}
3758

3859
// Forge is a forge to build container images.

wanda/forge_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,71 @@ func TestForgeWithRemoteWorkRepo(t *testing.T) {
455455
}
456456
}
457457

458+
func TestBuild_WithDeps(t *testing.T) {
459+
// Test: dep-top -> dep-middle -> dep-base
460+
// Build should build in order: dep-base, dep-middle, dep-top
461+
config := &ForgeConfig{
462+
WorkDir: "testdata",
463+
NamePrefix: "cr.ray.io/rayproject/",
464+
}
465+
466+
if err := Build("testdata/dep-top.wanda.yaml", config); err != nil {
467+
t.Fatalf("build with deps: %v", err)
468+
}
469+
470+
// Verify dep-top was built and can be read
471+
ref, err := name.ParseReference("dep-top")
472+
if err != nil {
473+
t.Fatalf("parse reference: %v", err)
474+
}
475+
476+
img, err := daemon.Image(ref)
477+
if err != nil {
478+
t.Fatalf("read dep-top image: %v", err)
479+
}
480+
481+
layers, err := img.Layers()
482+
if err != nil {
483+
t.Fatalf("read layers: %v", err)
484+
}
485+
486+
// Should have 3 layers: dep-base, dep-middle, dep-top
487+
if got, want := len(layers), 3; got != want {
488+
t.Errorf("got %d layers, want %d", got, want)
489+
}
490+
}
491+
492+
func TestBuild_NoDeps(t *testing.T) {
493+
// Test backward compatibility: a spec with no deps should work
494+
config := &ForgeConfig{
495+
WorkDir: "testdata",
496+
NamePrefix: "cr.ray.io/rayproject/",
497+
}
498+
499+
if err := Build("testdata/hello-test.wanda.yaml", config); err != nil {
500+
t.Fatalf("build with deps: %v", err)
501+
}
502+
503+
ref, err := name.ParseReference("cr.ray.io/rayproject/hello-test")
504+
if err != nil {
505+
t.Fatalf("parse reference: %v", err)
506+
}
507+
508+
img, err := daemon.Image(ref)
509+
if err != nil {
510+
t.Fatalf("read hello image: %v", err)
511+
}
512+
513+
layers, err := img.Layers()
514+
if err != nil {
515+
t.Fatalf("read layers: %v", err)
516+
}
517+
518+
if got, want := len(layers), 1; got != want {
519+
t.Errorf("got %d layers, want %d", got, want)
520+
}
521+
}
522+
458523
func TestForgeLocal_withNamePrefix(t *testing.T) {
459524
if runtime.GOOS != "linux" {
460525
t.Skip("skipping test on non-linux")

0 commit comments

Comments
 (0)