Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions pkg/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"fmt"
"os"
"path/filepath"

"github.com/Layr-Labs/devkit-cli/config/configs"
Expand All @@ -22,6 +23,10 @@ var BuildCommand = &cli.Command{
Name: "context",
Usage: "Select the context to use in this command (devnet, testnet or mainnet)",
},
&cli.StringFlag{
Name: "target",
Usage: "Override the build script target (relative or absolute path)",
},
}, common.GlobalFlags...),
Action: func(cCtx *cli.Context) error {
logger := common.LoggerFromContext(cCtx)
Expand Down Expand Up @@ -81,18 +86,43 @@ var BuildCommand = &cli.Command{
logger.Debug("Project Name: %s", cfg.Config.Project.Name)
logger.Debug("Building AVS components...")

// All scripts contained here
scriptsDir := filepath.Join(".devkit", "scripts")
// Resolve target script (defaults to .devkit/scripts/build)
defaultScript := filepath.Join(".devkit", "scripts", "build")
scriptPath := defaultScript

if override := cCtx.String("target"); override != "" {
if filepath.IsAbs(override) {
scriptPath = override
} else {
scriptPath = filepath.Join(".", filepath.Clean(override))
}

if _, statErr := os.Stat(scriptPath); statErr != nil {
if os.IsNotExist(statErr) {
return fmt.Errorf("custom build target not found: %s", scriptPath)
}
return fmt.Errorf("failed to access custom build target %s: %w", scriptPath, statErr)
}

logger.Info("Using custom build target: %s", scriptPath)
}

// Execute build via .devkit scripts with project name
output, err := common.CallTemplateScript(cCtx.Context, logger, dir, filepath.Join(scriptsDir, "build"), common.ExpectJSONResponse,
// Execute build via script with project name
params := [][]byte{
[]byte("--image"),
[]byte(cfg.Config.Project.Name),
[]byte("--tag"),
[]byte(version),
[]byte("--lang"),
[]byte(language),
)
}

// Forward any additional positional arguments to the build script
for _, arg := range cCtx.Args().Slice() {
params = append(params, []byte(arg))
}

output, err := common.CallTemplateScript(cCtx.Context, logger, dir, scriptPath, common.ExpectJSONResponse, params...)
if err != nil {
logger.Error("Build script failed with error: %v", err)
return fmt.Errorf("build failed: %w", err)
Expand Down
142 changes: 142 additions & 0 deletions pkg/commands/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package commands
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -204,3 +206,143 @@ echo "Mock build executed"`
t.Error("Build command did not exit after context cancellation")
}
}

func TestBuildCommand_CustomTarget(t *testing.T) {
tmpDir := t.TempDir()

// Create config directory and devnet.yaml
configDir := filepath.Join(tmpDir, "config")
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatal(err)
}
contextsDir := filepath.Join(configDir, "contexts")
if err := os.MkdirAll(contextsDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configs.ConfigYamls[configs.LatestVersion]), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(contextsDir, "devnet.yaml"), []byte(contexts.ContextYamls[contexts.LatestVersion]), 0644); err != nil {
t.Fatal(err)
}

// Create custom userland build script
userlandDir := filepath.Join(tmpDir, "userland")
if err := os.MkdirAll(userlandDir, 0755); err != nil {
t.Fatal(err)
}
capturePath := filepath.Join(tmpDir, "captured_args.txt")
customScript := fmt.Sprintf(`#!/bin/bash
echo "$@" > "%s"
cat <<'EOF'
{"artifact":{"artifactId":"custom-artifact","component":"custom-component"}}
EOF
`, capturePath)
customScriptPath := filepath.Join(userlandDir, "build")
if err := os.WriteFile(customScriptPath, []byte(customScript), 0755); err != nil {
t.Fatal(err)
}

oldWd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
defer func() {
if err := os.Chdir(oldWd); err != nil {
t.Logf("Failed to restore original directory: %v", err)
}
}()

app := &cli.App{
Name: "test",
Commands: []*cli.Command{testutils.WithTestConfigAndNoopLogger(BuildCommand)},
}

args := []string{
"app", "build",
"--context", devnet.DEVNET_CONTEXT,
"--target", "userland/build",
"--",
"--dockerfile", "path/to/Dockerfile",
"--context", "custom-performer",
}

if err := app.Run(args); err != nil {
t.Fatalf("Failed to execute build command with custom target: %v", err)
}

// Ensure custom script captured the forwarded arguments
captured, err := os.ReadFile(capturePath)
if err != nil {
t.Fatalf("Failed to read captured args: %v", err)
}
capturedArgs := strings.TrimSpace(string(captured))
if !strings.Contains(capturedArgs, "--dockerfile path/to/Dockerfile") {
t.Fatalf("Expected forwarded args to include dockerfile flag, got: %s", capturedArgs)
}
if !strings.Contains(capturedArgs, "--context custom-performer") {
t.Fatalf("Expected forwarded args to include performer context flag, got: %s", capturedArgs)
}

// Context file should have been updated with custom artifact info
contextBytes, err := os.ReadFile(filepath.Join(configDir, "contexts", "devnet.yaml"))
if err != nil {
t.Fatalf("Failed to read context file: %v", err)
}
contextStr := string(contextBytes)
if !strings.Contains(contextStr, "artifactId: custom-artifact") {
t.Fatalf("Expected context to include custom artifactId, got: %s", contextStr)
}
if !strings.Contains(contextStr, "component: custom-component") {
t.Fatalf("Expected context to include custom component, got: %s", contextStr)
}
}

func TestBuildCommand_CustomTargetMissing(t *testing.T) {
tmpDir := t.TempDir()

// Create config directory and devnet.yaml
configDir := filepath.Join(tmpDir, "config")
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatal(err)
}
contextsDir := filepath.Join(configDir, "contexts")
if err := os.MkdirAll(contextsDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configs.ConfigYamls[configs.LatestVersion]), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(contextsDir, "devnet.yaml"), []byte(contexts.ContextYamls[contexts.LatestVersion]), 0644); err != nil {
t.Fatal(err)
}

oldWd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
defer func() {
if err := os.Chdir(oldWd); err != nil {
t.Logf("Failed to restore original directory: %v", err)
}
}()

app := &cli.App{
Name: "test",
Commands: []*cli.Command{testutils.WithTestConfigAndNoopLogger(BuildCommand)},
}

err = app.Run([]string{"app", "build", "--context", devnet.DEVNET_CONTEXT, "--target", "userland/build"})
if err == nil {
t.Fatal("Expected error due to missing custom target, got nil")
}
if !strings.Contains(err.Error(), "custom build target not found") {
t.Fatalf("Expected missing target error, got: %v", err)
}
}
Loading