Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cosmwasm contract build flow during e2e test execution #782

Merged
merged 20 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from 17 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
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
}
}
119 changes: 119 additions & 0 deletions contract/cosmwasm/compile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
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"
)

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

// 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: "/target",
Reecepbcups marked this conversation as resolved.
Show resolved Hide resolved
},
{
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 coyympilation is complete, this can be called after chain setup
misko9 marked this conversation as resolved.
Show resolved Hide resolved
// 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