Skip to content

Commit

Permalink
feat: cosmwasm contract build flow during e2e test execution (#782)
Browse files Browse the repository at this point in the history
* Initial cosmwasm build flow

* Add example cosmwasm test

* Add comment

* Move cosmwasm.go to interchaintest package

* Rename intro to rust-optimizer

* Allow cw contract to compile with custom docker image and version

* Scaffold workspace-optimizer and move cosmwasm to own package

* workspace-optimizer working, updated comments

* Return channels when compiling contracts so that chain setup can proceed
in parallel.

* Cleanup waiting for compilation

* fix lint errors

* Add example cosmwasm tests to ci

* Revert "Add example cosmwasm tests to ci"

This reverts commit ecd6e62.

* Check for rust/workspace-optimizer version and set the cache directory
appropriately

* Fix comment

Co-authored-by: Reece Williams <[email protected]>

---------

Co-authored-by: Reece Williams <[email protected]>
  • Loading branch information
misko9 and Reecepbcups authored Oct 13, 2023
1 parent fcfcac6 commit 08b20f6
Show file tree
Hide file tree
Showing 38 changed files with 3,859 additions and 1 deletion.
2 changes: 1 addition & 1 deletion chain/cosmos/wasm/wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ func WasmEncoding() *testutil.TestEncodingConfig {
wasmtypes.RegisterInterfaces(cfg.InterfaceRegistry)

return &cfg
}
}
134 changes: 134 additions & 0 deletions contract/cosmwasm/compile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package cosmwasm

import (
"context"
"path/filepath"
"fmt"
"runtime"
"io"
"os"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/stdcopy"
"github.com/hashicorp/go-version"
)

// compile will compile the specified repo using the specified docker image and version
func compile(image string, optVersion string, repoPath string) (string, error) {
// Set the image to pull/use
arch := ""
if runtime.GOARCH == "arm64" {
arch = "-arm64"
}
imageFull := image + arch + ":" + optVersion

// Check if version is less than 0.13.0, if so, use old cache directory
cacheDir := "/target"
versionThresh, err := version.NewVersion("0.13.0")
if err != nil {
return "", fmt.Errorf("version threshold 0.13.0: %w", err)
}
myVersion, err := version.NewVersion(optVersion)
if err != nil {
return "", fmt.Errorf("version %s: %w", optVersion, err)
}
if myVersion.LessThan(versionThresh) {
cacheDir = "/code/target"
}

// Get absolute path of contract project
pwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("getwd: %w", err)
}
repoPathFull := filepath.Join(pwd, repoPath)

ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return "", fmt.Errorf("new client with opts: %w", err)
}
defer cli.Close()

reader, err := cli.ImagePull(ctx, imageFull, types.ImagePullOptions{})
if err != nil {
return "", fmt.Errorf("pull image %s: %w", imageFull, err)
}

defer reader.Close()
_, err = io.Copy(os.Stdout, reader)
if err != nil {
return "", fmt.Errorf("io copy %s: %w", imageFull, err)
}

resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: imageFull,
Tty: false,
}, &container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: repoPathFull,
Target: "/code",
},
{
Type: mount.TypeVolume,
Source: filepath.Base(repoPathFull)+"_cache",
Target: cacheDir,
},
{
Type: mount.TypeVolume,
Source: "registry_cache",
Target: "/usr/local/cargo/registry",
},
},
}, nil, nil, "")
if err != nil {
return "", fmt.Errorf("create container %s: %w", imageFull, err)
}

if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
return "", fmt.Errorf("start container %s: %w", imageFull, err)
}

statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
select {
case err := <-errCh:
if err != nil {
return "", fmt.Errorf("wait container %s: %w", imageFull, err)
}
case <-statusCh:
}

out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true})
if err != nil {
return "", fmt.Errorf("logs container %s: %w", imageFull, err)
}

_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, out)
if err != nil {
return "", fmt.Errorf("std copy %s: %w", imageFull, err)
}

err = cli.ContainerStop(ctx, resp.ID, container.StopOptions{})
if err != nil {
// Only return the error if it didn't match an already stopped, or a missing container.
if !(errdefs.IsNotModified(err) || errdefs.IsNotFound(err)) {
return "", fmt.Errorf("stop container %s: %w", imageFull, err)
}
}

err = cli.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{
Force: true,
RemoveVolumes: true,
})
if err != nil && !errdefs.IsNotFound(err) {
return "", fmt.Errorf("remove container %s: %w", imageFull, err)
}

return repoPathFull, nil
}
86 changes: 86 additions & 0 deletions contract/cosmwasm/rust_optimizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package cosmwasm

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

type Contract struct {
DockerImage string
Version string
RelativePath string
wasmBinPathChan chan string
errChan chan error
}

// NewContract return a contract struct, populated with defaults and its relative path
// relativePath is the relative path to the contract on local machine
func NewContract(relativePath string) *Contract {
return &Contract{
DockerImage: "cosmwasm/rust-optimizer",
Version: "0.14.0",
RelativePath: relativePath,
}
}

// WithDockerImage sets a custom docker image to use
func (c *Contract) WithDockerImage(image string) *Contract {
c.DockerImage = image
return c
}

// WithVersion sets a custom version to use
func (c *Contract) WithVersion(version string) *Contract {
c.Version = version
return c
}

// Compile will compile the contract
// cosmwasm/rust-optimizer is the expected docker image
func (c *Contract) Compile() *Contract {
c.wasmBinPathChan = make(chan string)
c.errChan = make(chan error, 1)

go func() {
repoPathFull, err := compile(c.DockerImage, c.Version, c.RelativePath)
if err != nil {
c.errChan <- err
return
}

// Form the path to the artifacts directory, used for checksum.txt and package.wasm
artifactsPath := filepath.Join(repoPathFull, "artifacts")

// Parse the checksums.txt for the crate/wasm binary name
checksumsPath := filepath.Join(artifactsPath, "checksums.txt")
checksumsBz, err := os.ReadFile(checksumsPath)
if err != nil {
c.errChan <- fmt.Errorf("checksums read: %w", err)
return
}
_, wasmBin, found := strings.Cut(string(checksumsBz), " ")
if !found {
c.errChan <- fmt.Errorf("wasm binary name not found")
return
}

// Form the path to the wasm binary
c.wasmBinPathChan <- filepath.Join(artifactsPath, strings.TrimSpace(wasmBin))
}()

return c
}

// WaitForCompile will wait until compilation is complete, this can be called after chain setup
// Successful compilation will return the binary location in a channel
func (c *Contract) WaitForCompile() (string, error) {
contractBinary := ""
select {
case err := <-c.errChan:
return "", err
case contractBinary = <-c.wasmBinPathChan:
}
return contractBinary, nil
}
98 changes: 98 additions & 0 deletions contract/cosmwasm/workspace_optimizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cosmwasm

import (
"bufio"
"path/filepath"
"fmt"
"os"
"strings"
)

type Workspace struct {
DockerImage string
Version string
RelativePath string
wasmBinariesChan chan map[string]string
errChan chan error
}

// NewWorkspace returns a workspace struct, populated with defaults and its relative path
// relativePath is the relative path to the workspace on local machine
func NewWorkspace(relativePath string) *Workspace {
return &Workspace{
DockerImage: "cosmwasm/workspace-optimizer",
Version: "0.14.0",
RelativePath: relativePath,
}
}

// WithDockerImage sets a custom docker image to use
func (w *Workspace) WithDockerImage(image string) *Workspace {
w.DockerImage = image
return w
}

// WithVersion sets a custom version to use
func (w *Workspace) WithVersion(version string) *Workspace {
w.Version = version
return w
}

// Compile will compile the workspace's contracts
// cosmwasm/workspace-optimizer is the expected docker image
// The workspace object is returned, call WaitForCompile() to get results
func (w *Workspace) Compile() *Workspace {
w.wasmBinariesChan = make(chan map[string]string)
w.errChan = make(chan error, 1)

go func() {
repoPathFull, err := compile(w.DockerImage, w.Version, w.RelativePath)
if err != nil {
w.errChan <- err
return
}

// Form the path to the artifacts directory, used for checksum.txt and package.wasm
artifactsPath := filepath.Join(repoPathFull, "artifacts")

// Parse the checksums.txt for the crate/wasm binary names
wasmBinaries := make(map[string]string)
checksumsPath := filepath.Join(artifactsPath, "checksums.txt")
file, err := os.Open(checksumsPath)
if err != nil {
w.errChan <- fmt.Errorf("checksums open: %w", err)
return
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
_, wasmBin, found := strings.Cut(line, " ")
if !found {
w.errChan <- fmt.Errorf("wasm binary name not found")
return
}
wasmBin = strings.TrimSpace(wasmBin)
crateName, _, found := strings.Cut(wasmBin, ".")
if !found {
w.errChan <- fmt.Errorf("wasm binary name invalid")
return
}
wasmBinaries[crateName] = filepath.Join(artifactsPath, wasmBin)
}
w.wasmBinariesChan <- wasmBinaries
}()

return w
}

// WaitForCompile will wait until coyympilation is complete, this can be called after chain setup
// Successful compilation will return a map of crate names to binary locations in a channel
func (w *Workspace) WaitForCompile() (map[string]string, error) {
contractBinaries := make(map[string]string)
select {
case err := <-w.errChan:
return contractBinaries, err
case contractBinaries = <-w.wasmBinariesChan:
}
return contractBinaries, nil
}
3 changes: 3 additions & 0 deletions examples/cosmwasm/rust-optimizer/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The rust-optimizer example contains a simple contract that performs minimal functionality.
The single contract uses cosmwasm/rust-optimizer to compile during test execution.
The test case shows how the contract source can integrate with interchaintest: building the contract, spinning up a chain, storing it on-chain, and instantiating/querying/executing against it.
2 changes: 2 additions & 0 deletions examples/cosmwasm/rust-optimizer/contract/.cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[alias]
wasm = "build --target wasm32-unknown-unknown --release --lib"
2 changes: 2 additions & 0 deletions examples/cosmwasm/rust-optimizer/contract/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
artifacts/
Loading

0 comments on commit 08b20f6

Please sign in to comment.