Skip to content

Commit e632ccc

Browse files
feat(wanda): Add BuildWithDeps to build dependency chains
Implement build integration for wanda dependency resolution. The CLI now automatically builds all dependencies in topological order before building the target spec. - Add BuildWithDeps function that builds specs in dependency order - Tag each built image with spec.Name for @ref resolution - Update CLI to use BuildWithDeps (backward compatible) - In RayCI mode, skip dep building (handled by prior pipeline steps) - Add test fixtures and tests for dependency chain builds Topic: wanda-build-deps Relative: wanda-deps Labels: draft Generated with help from Claude Opus 4.5 Signed-off-by: andrew <andrew@anyscale.com>
1 parent d7807b5 commit e632ccc

File tree

9 files changed

+119
-4
lines changed

9 files changed

+119
-4
lines changed

wanda/forge.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,64 @@ func Build(specFile string, config *ForgeConfig) error {
3535
return forge.Build(spec)
3636
}
3737

38+
// BuildWithDeps builds a spec and all its dependencies in topological order.
39+
// In RayCI mode, deps are assumed built by prior pipeline steps; only the root is built.
40+
func BuildWithDeps(specFile string, config *ForgeConfig) error {
41+
if config == nil {
42+
config = &ForgeConfig{}
43+
}
44+
45+
// In RayCI mode, only build the root spec (deps built by prior steps)
46+
if config.RayCI {
47+
return Build(specFile, config)
48+
}
49+
50+
graph, err := BuildDepGraph(specFile, os.LookupEnv)
51+
if err != nil {
52+
return fmt.Errorf("build dep graph: %w", err)
53+
}
54+
55+
if err := graph.ValidateDeps(); err != nil {
56+
return fmt.Errorf("validate deps: %w", err)
57+
}
58+
59+
forge, err := NewForge(config)
60+
if err != nil {
61+
return fmt.Errorf("make forge: %w", err)
62+
}
63+
64+
for _, name := range graph.Order() {
65+
rs := graph.Get(name)
66+
spec := rs.Spec
67+
68+
log.Printf("building %s (from %s)", name, rs.Path)
69+
70+
if err := forge.buildWithLocalTag(spec); err != nil {
71+
return fmt.Errorf("build %s: %w", name, err)
72+
}
73+
}
74+
75+
return nil
76+
}
77+
78+
// buildWithLocalTag builds a spec and ensures it's tagged with spec.Name
79+
// so that @name references can resolve it.
80+
func (f *Forge) buildWithLocalTag(spec *Spec) error {
81+
if !hasTag(spec.Tags, spec.Name) {
82+
spec.Tags = append(spec.Tags, spec.Name)
83+
}
84+
return f.Build(spec)
85+
}
86+
87+
func hasTag(tags []string, tag string) bool {
88+
for _, t := range tags {
89+
if t == tag {
90+
return true
91+
}
92+
}
93+
return false
94+
}
95+
3896
// Forge is a forge to build container images.
3997
type Forge struct {
4098
config *ForgeConfig

wanda/forge_test.go

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

458+
func TestBuildWithDeps(t *testing.T) {
459+
config := &ForgeConfig{WorkDir: "testdata"}
460+
461+
if err := BuildWithDeps("testdata/dep-top.wanda.yaml", config); err != nil {
462+
t.Fatalf("BuildWithDeps: %v", err)
463+
}
464+
465+
// Verify dep-top was built and contains content from all layers
466+
ref, err := name.ParseReference("dep-top")
467+
if err != nil {
468+
t.Fatalf("parse reference: %v", err)
469+
}
470+
471+
img, err := daemon.Image(ref)
472+
if err != nil {
473+
t.Fatalf("read image: %v", err)
474+
}
475+
476+
layers, err := img.Layers()
477+
if err != nil {
478+
t.Fatalf("read layers: %v", err)
479+
}
480+
481+
// Should have 3 layers: dep-base, dep-middle, dep-top
482+
if len(layers) != 3 {
483+
t.Fatalf("got %d layers, want 3", len(layers))
484+
}
485+
486+
// Check that dep-middle's world.txt is in the image
487+
files, err := filesInLayer(layers[1])
488+
if err != nil {
489+
t.Fatalf("read layer: %v", err)
490+
}
491+
492+
if got := files["opt/app/world.txt"]; got != worldDotTxt {
493+
t.Errorf("world.txt in image, got %q, want %q", got, worldDotTxt)
494+
}
495+
}
496+
497+
func TestBuildWithDeps_NoDeps(t *testing.T) {
498+
config := &ForgeConfig{WorkDir: "testdata"}
499+
500+
// BuildWithDeps should work for specs without deps (backward compat)
501+
if err := BuildWithDeps("testdata/hello.wanda.yaml", config); err != nil {
502+
t.Fatalf("BuildWithDeps: %v", err)
503+
}
504+
}
505+
458506
func TestForgeLocal_withNamePrefix(t *testing.T) {
459507
if runtime.GOOS != "linux" {
460508
t.Skip("skipping test on non-linux")

wanda/testdata/Dockerfile.dep-base

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM scratch
2+
3+
COPY Dockerfile.dep-base /opt/app/Dockerfile
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM dep-base
2+
3+
COPY world.txt /opt/app/world.txt

wanda/testdata/Dockerfile.dep-top

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM dep-middle
2+
3+
COPY Dockerfile.dep-top /opt/app/top.txt

wanda/testdata/dep-base.wanda.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
name: dep-base
2-
dockerfile: Dockerfile.hello
2+
dockerfile: Dockerfile.dep-base
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: dep-middle
22
deps: [dep-base.wanda.yaml]
33
froms: ["@dep-base"]
4-
dockerfile: Dockerfile.world
4+
dockerfile: Dockerfile.dep-middle
55
srcs:
66
- world.txt

wanda/testdata/dep-top.wanda.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
name: dep-top
22
deps: [dep-middle.wanda.yaml]
33
froms: ["@dep-middle"]
4-
dockerfile: Dockerfile.local
4+
dockerfile: Dockerfile.dep-top

wanda/wanda/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func main() {
8585
ReadOnlyCache: *readOnly,
8686
}
8787

88-
if err := wanda.Build(input, config); err != nil {
88+
if err := wanda.BuildWithDeps(input, config); err != nil {
8989
log.Fatal(err)
9090
}
9191
}

0 commit comments

Comments
 (0)