Commit 56502ddc authored by Inphi's avatar Inphi Committed by GitHub

cannon: Multi VM executor (#12072)

* cannon: Multi VM executor

* fix run subcmd arg fwding

* fix mt prestate

* add list subcmd; multicannon in op-stack-go

* remove cannon-latest

* safer strconv

* lint

* include .gitkeep in embed

* fix .git copy

* add detect.go tests

* add nosemgrep

* review comments

* list filtering

* add note to MIPS.sol in version stf ref

* use fork-exec

* minimal flag parsing

* load old cannon binaries from docker images

* note

* --help flag defaults

* remove redundant copy from cannon-builder-0
parent 30725498
......@@ -142,13 +142,13 @@ $(DEVNET_CANNON_PRESTATE_FILES):
make cannon-prestate-mt
cannon-prestate: op-program cannon ## Generates prestate using cannon and op-program
./cannon/bin/cannon load-elf --path op-program/bin/op-program-client.elf --out op-program/bin/prestate.json --meta op-program/bin/meta.json
./cannon/bin/cannon load-elf --type singlethreaded --path op-program/bin/op-program-client.elf --out op-program/bin/prestate.json --meta op-program/bin/meta.json
./cannon/bin/cannon run --proof-at '=0' --stop-at '=1' --input op-program/bin/prestate.json --meta op-program/bin/meta.json --proof-fmt 'op-program/bin/%d.json' --output ""
mv op-program/bin/0.json op-program/bin/prestate-proof.json
.PHONY: cannon-prestate
cannon-prestate-mt: op-program cannon ## Generates prestate using cannon and op-program in the multithreaded cannon format
./cannon/bin/cannon load-elf --type cannon-mt --path op-program/bin/op-program-client.elf --out op-program/bin/prestate-mt.bin.gz --meta op-program/bin/meta-mt.json
./cannon/bin/cannon load-elf --type multithreaded --path op-program/bin/op-program-client.elf --out op-program/bin/prestate-mt.bin.gz --meta op-program/bin/meta-mt.json
./cannon/bin/cannon run --proof-at '=0' --stop-at '=1' --input op-program/bin/prestate-mt.bin.gz --meta op-program/bin/meta-mt.json --proof-fmt 'op-program/bin/%d-mt.json' --output ""
mv op-program/bin/0-mt.json op-program/bin/prestate-proof-mt.json
.PHONY: cannon-prestate-mt
......
......@@ -13,3 +13,4 @@ state.json
*.pprof
*.out
bin
multicannon/embeds/cannon*
......@@ -13,8 +13,15 @@ ifeq ($(shell uname),Darwin)
FUZZLDFLAGS := -ldflags=-extldflags=-Wl,-ld_classic
endif
cannon:
env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/cannon .
cannon-impl:
env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/cannon-impl .
cannon-embeds: cannon-impl
@cp bin/cannon-impl ./multicannon/embeds/cannon-0
@cp bin/cannon-impl ./multicannon/embeds/cannon-1
cannon: cannon-embeds
env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/cannon ./multicannon/
clean:
rm -rf bin
......
......@@ -30,7 +30,7 @@ make cannon
# Transform MIPS op-program client binary into first VM state.
# This outputs state.json (VM state) and meta.json (for debug symbols).
./bin/cannon load-elf --path=../op-program/bin/op-program-client.elf
./bin/cannon load-elf --type singlethreaded --path=../op-program/bin/op-program-client.elf
# Run cannon emulator (with example inputs)
# Note that the server-mode op-program command is passed into cannon (after the --),
......
......@@ -12,6 +12,7 @@ import (
"github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/versions"
"github.com/ethereum-optimism/optimism/cannon/serialize"
openum "github.com/ethereum-optimism/optimism/op-service/enum"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
"github.com/ethereum-optimism/optimism/op-service/jsonutil"
)
......@@ -19,9 +20,8 @@ import (
var (
LoadELFVMTypeFlag = &cli.StringFlag{
Name: "type",
Usage: "VM type to create state for. Options are 'cannon' (default), 'cannon-mt'",
Value: "cannon",
Required: false,
Usage: "VM type to create state for. Valid options: " + openum.EnumString(stateVersions()),
Required: true,
}
LoadELFPathFlag = &cli.PathFlag{
Name: "path",
......@@ -43,21 +43,12 @@ var (
}
)
type VMType string
var (
cannonVMType VMType = "cannon"
mtVMType VMType = "cannon-mt"
)
func vmTypeFromString(ctx *cli.Context) (VMType, error) {
if vmTypeStr := ctx.String(LoadELFVMTypeFlag.Name); vmTypeStr == string(cannonVMType) {
return cannonVMType, nil
} else if vmTypeStr == string(mtVMType) {
return mtVMType, nil
} else {
return "", fmt.Errorf("unknown VM type %q", vmTypeStr)
func stateVersions() []string {
vers := make([]string, len(versions.StateVersionTypes))
for i, v := range versions.StateVersionTypes {
vers[i] = v.String()
}
return vers
}
func LoadELF(ctx *cli.Context) error {
......@@ -73,9 +64,12 @@ func LoadELF(ctx *cli.Context) error {
var createInitialState func(f *elf.File) (mipsevm.FPVMState, error)
var patcher = program.PatchStack
if vmType, err := vmTypeFromString(ctx); err != nil {
ver, err := versions.ParseStateVersion(ctx.String(LoadELFVMTypeFlag.Name))
if err != nil {
return err
} else if vmType == cannonVMType {
}
switch ver {
case versions.VersionSingleThreaded:
createInitialState = func(f *elf.File) (mipsevm.FPVMState, error) {
return program.LoadELF(f, singlethreaded.CreateInitialState)
}
......@@ -86,12 +80,12 @@ func LoadELF(ctx *cli.Context) error {
}
return program.PatchStack(state)
}
} else if vmType == mtVMType {
case versions.VersionMultiThreaded:
createInitialState = func(f *elf.File) (mipsevm.FPVMState, error) {
return program.LoadELF(f, multithreaded.CreateInitialState)
}
} else {
return fmt.Errorf("invalid VM type: %q", vmType)
default:
return fmt.Errorf("unsupported state version: %d (%s)", ver, ver.String())
}
state, err := createInitialState(elfProgram)
......@@ -118,15 +112,19 @@ func LoadELF(ctx *cli.Context) error {
return serialize.Write(ctx.Path(LoadELFOutFlag.Name), versionedState, OutFilePerm)
}
var LoadELFCommand = &cli.Command{
func CreateLoadELFCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "load-elf",
Usage: "Load ELF file into Cannon state",
Description: "Load ELF file into Cannon state",
Action: LoadELF,
Action: action,
Flags: []cli.Flag{
LoadELFVMTypeFlag,
LoadELFPathFlag,
LoadELFOutFlag,
LoadELFMetaFlag,
},
}
}
var LoadELFCommand = CreateLoadELFCommand(LoadELF)
......@@ -496,11 +496,12 @@ func Run(ctx *cli.Context) error {
return nil
}
var RunCommand = &cli.Command{
func CreateRunCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "run",
Usage: "Run VM step(s) and generate proof data to replicate onchain.",
Description: "Run VM step(s) and generate proof data to replicate onchain. See flags to match when to output a proof, a snapshot, or to stop early.",
Action: Run,
Action: action,
Flags: []cli.Flag{
RunInputFlag,
RunOutputFlag,
......@@ -518,4 +519,7 @@ var RunCommand = &cli.Command{
RunDebugFlag,
RunDebugInfoFlag,
},
}
}
var RunCommand = CreateRunCommand(Run)
......@@ -39,13 +39,17 @@ func Witness(ctx *cli.Context) error {
return nil
}
var WitnessCommand = &cli.Command{
func CreateWitnessCommand(action cli.ActionFunc) *cli.Command {
return &cli.Command{
Name: "witness",
Usage: "Convert a Cannon JSON state into a binary witness",
Description: "Convert a Cannon JSON state into a binary witness. The hash of the witness is written to stdout",
Action: Witness,
Action: action,
Flags: []cli.Flag{
WitnessInputFlag,
WitnessOutputFlag,
},
}
}
var WitnessCommand = CreateWitnessCommand(Witness)
package versions
import (
"fmt"
"io"
"github.com/ethereum-optimism/optimism/cannon/serialize"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
)
func DetectVersion(path string) (StateVersion, error) {
if !serialize.IsBinaryFile(path) {
return VersionSingleThreaded, nil
}
var f io.ReadCloser
f, err := ioutil.OpenDecompressed(path)
if err != nil {
return 0, fmt.Errorf("failed to open file %q: %w", path, err)
}
defer f.Close()
var ver StateVersion
bin := serialize.NewBinaryReader(f)
if err := bin.ReadUInt(&ver); err != nil {
return 0, err
}
switch ver {
case VersionSingleThreaded, VersionMultiThreaded:
return ver, nil
default:
return 0, fmt.Errorf("%w: %d", ErrUnknownVersion, ver)
}
}
package versions
import (
"os"
"path/filepath"
"testing"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/multithreaded"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/singlethreaded"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
"github.com/stretchr/testify/require"
)
func TestDetectVersion(t *testing.T) {
t.Run("SingleThreadedJSON", func(t *testing.T) {
state, err := NewFromState(singlethreaded.CreateEmptyState())
require.NoError(t, err)
path := writeToFile(t, "state.json", state)
version, err := DetectVersion(path)
require.NoError(t, err)
require.Equal(t, VersionSingleThreaded, version)
})
t.Run("SingleThreadedBinary", func(t *testing.T) {
state, err := NewFromState(singlethreaded.CreateEmptyState())
require.NoError(t, err)
path := writeToFile(t, "state.bin.gz", state)
version, err := DetectVersion(path)
require.NoError(t, err)
require.Equal(t, VersionSingleThreaded, version)
})
t.Run("MultiThreadedBinary", func(t *testing.T) {
state, err := NewFromState(multithreaded.CreateEmptyState())
require.NoError(t, err)
path := writeToFile(t, "state.bin.gz", state)
version, err := DetectVersion(path)
require.NoError(t, err)
require.Equal(t, VersionMultiThreaded, version)
})
}
func TestDetectVersionInvalid(t *testing.T) {
t.Run("bad gzip", func(t *testing.T) {
dir := t.TempDir()
filename := "state.bin.gz"
path := filepath.Join(dir, filename)
require.NoError(t, os.WriteFile(path, []byte("ekans"), 0o644))
_, err := DetectVersion(path)
require.ErrorContains(t, err, "failed to open file")
})
t.Run("unknown version", func(t *testing.T) {
dir := t.TempDir()
filename := "state.bin.gz"
path := filepath.Join(dir, filename)
const badVersion = 0xFF
err := ioutil.WriteCompressedBytes(path, []byte{badVersion}, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
require.NoError(t, err)
_, err = DetectVersion(path)
require.ErrorIs(t, err, ErrUnknownVersion)
})
}
......@@ -16,6 +16,7 @@ import (
type StateVersion uint8
const (
// VersionSingleThreaded is the version of the Cannon STF found in op-contracts/v1.6.0 - https://github.com/ethereum-optimism/optimism/blob/op-contracts/v1.6.0/packages/contracts-bedrock/src/cannon/MIPS.sol
VersionSingleThreaded StateVersion = iota
VersionMultiThreaded
)
......@@ -25,6 +26,8 @@ var (
ErrJsonNotSupported = errors.New("json not supported")
)
var StateVersionTypes = []StateVersion{VersionSingleThreaded, VersionMultiThreaded}
func LoadStateFromFile(path string) (*VersionedState, error) {
if !serialize.IsBinaryFile(path) {
// Always use singlethreaded for JSON states
......@@ -103,3 +106,25 @@ func (s *VersionedState) MarshalJSON() ([]byte, error) {
}
return json.Marshal(s.FPVMState)
}
func (s StateVersion) String() string {
switch s {
case VersionSingleThreaded:
return "singlethreaded"
case VersionMultiThreaded:
return "multithreaded"
default:
return "unknown"
}
}
func ParseStateVersion(ver string) (StateVersion, error) {
switch ver {
case "singlethreaded":
return VersionSingleThreaded, nil
case "multithreaded":
return VersionMultiThreaded, nil
default:
return StateVersion(0), errors.New("unknown state version")
}
}
package main
import (
"context"
"embed"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/versions"
)
// use the all directive to ensure the .gitkeep file is retained and avoid compiler errors
//go:embed all:embeds
var vmFS embed.FS
const baseDir = "embeds"
func ExecuteCannon(ctx context.Context, args []string, ver versions.StateVersion) error {
switch ver {
case versions.VersionSingleThreaded, versions.VersionMultiThreaded:
default:
return errors.New("unsupported version")
}
cannonProgramName := vmFilename(ver)
cannonProgramBin, err := vmFS.ReadFile(cannonProgramName)
if err != nil {
return err
}
cannonProgramPath, err := extractTempFile(filepath.Base(cannonProgramName), cannonProgramBin)
if err != nil {
fmt.Fprintf(os.Stderr, "Error extracting %s: %v\n", cannonProgramName, err)
os.Exit(1)
}
defer os.Remove(cannonProgramPath)
if err := os.Chmod(cannonProgramPath, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error setting execute permission for %s: %v\n", cannonProgramName, err)
os.Exit(1)
}
// nosemgrep: go.lang.security.audit.dangerous-exec-command.dangerous-exec-command
cmd := exec.CommandContext(ctx, cannonProgramPath, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Start()
if err != nil {
return fmt.Errorf("unable to launch cannon-impl program: %w", err)
}
if err := cmd.Wait(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
// relay exit code to the parent process
os.Exit(exitErr.ExitCode())
} else {
return fmt.Errorf("failed to wait for cannon-impl program: %w", err)
}
}
return nil
}
func extractTempFile(name string, data []byte) (string, error) {
tempDir := os.TempDir()
tempFile, err := os.CreateTemp(tempDir, name+"-*")
if err != nil {
return "", err
}
defer tempFile.Close()
if _, err := tempFile.Write(data); err != nil {
return "", err
}
return tempFile.Name(), nil
}
func vmFilename(ver versions.StateVersion) string {
return fmt.Sprintf("%s/cannon-%d", baseDir, ver)
}
package main
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/urfave/cli/v2"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/versions"
)
func List(ctx *cli.Context) error {
return list()
}
func list() error {
fmt.Println("Available cannon versions:")
artifacts, err := getArtifacts()
if err != nil {
return err
}
for _, art := range artifacts {
if art.isValid() {
fmt.Printf("filename: %s\tversion: %s (%d)\n", art.filename, versions.StateVersion(art.ver), art.ver)
} else {
fmt.Printf("filename: %s\tversion: %s\n", art.filename, "unknown")
}
}
return nil
}
func getArtifacts() ([]artifact, error) {
var ret []artifact
entries, err := vmFS.ReadDir(baseDir)
if err != nil {
return nil, err
}
for _, entry := range entries {
filename := entry.Name()
toks := strings.Split(filename, "-")
if len(toks) != 2 {
continue
}
if toks[0] != "cannon" {
continue
}
ver, err := strconv.ParseUint(toks[1], 10, 8)
if err != nil {
ret = append(ret, artifact{filename, math.MaxUint64})
continue
}
ret = append(ret, artifact{filename, ver})
}
return ret, nil
}
type artifact struct {
filename string
ver uint64
}
func (a artifact) isValid() bool {
return a.ver != math.MaxUint64
}
var ListCommand = &cli.Command{
Name: "list",
Usage: "List embedded Cannon VM implementations",
Description: "List embedded Cannon VM implementations",
Action: List,
}
package main
import (
"fmt"
"os"
"github.com/ethereum-optimism/optimism/cannon/cmd"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/versions"
"github.com/urfave/cli/v2"
)
func LoadELF(ctx *cli.Context) error {
if len(os.Args) == 2 && os.Args[2] == "--help" {
if err := list(); err != nil {
return err
}
fmt.Println("use `--type <vm type> --help` to get more detailed help")
}
typ, err := parseFlag(os.Args[1:], "--type")
if err != nil {
return err
}
ver, err := versions.ParseStateVersion(typ)
if err != nil {
return err
}
return ExecuteCannon(ctx.Context, os.Args[1:], ver)
}
var LoadELFCommand = cmd.CreateLoadELFCommand(LoadELF)
package main
import (
"context"
"errors"
"fmt"
"os"
"github.com/ethereum-optimism/optimism/op-service/ctxinterrupt"
"github.com/urfave/cli/v2"
)
func main() {
app := cli.NewApp()
app.Name = "multicannon"
app.Usage = "MIPS Fault Proof tool"
app.Description = "MIPS Fault Proof tool"
app.Commands = []*cli.Command{
LoadELFCommand,
WitnessCommand,
RunCommand,
ListCommand,
}
ctx := ctxinterrupt.WithCancelOnInterrupt(context.Background())
err := app.RunContext(ctx, os.Args)
if err != nil {
if errors.Is(err, ctx.Err()) {
_, _ = fmt.Fprintf(os.Stderr, "command interrupted")
os.Exit(130)
} else {
_, _ = fmt.Fprintf(os.Stderr, "error: %v", err)
os.Exit(1)
}
}
}
package main
import (
"fmt"
"os"
"github.com/urfave/cli/v2"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/versions"
)
func Run(ctx *cli.Context) error {
fmt.Printf("args %v\n", os.Args[:])
if len(os.Args) == 3 && os.Args[2] == "--help" {
if err := list(); err != nil {
return err
}
fmt.Println("use `--input <valid input file> --help` to get more detailed help")
}
inputPath, err := parsePathFlag(os.Args[1:], "--input")
if err != nil {
return err
}
version, err := versions.DetectVersion(inputPath)
if err != nil {
return err
}
return ExecuteCannon(ctx.Context, os.Args[1:], version)
}
// var RunCommand = cmd.CreateRunCommand(Run)
var RunCommand = &cli.Command{
Name: "run",
Usage: "Run VM step(s) and generate proof data to replicate onchain.",
Description: "Run VM step(s) and generate proof data to replicate onchain. See flags to match when to output a proof, a snapshot, or to stop early.",
Action: Run,
SkipFlagParsing: true,
}
package main
import (
"errors"
"fmt"
"os"
"strings"
)
// parseFlag reads a flag argument. It assumes the flag has an argument
func parseFlag(args []string, flag string) (string, error) {
for i := 0; i < len(args); i++ {
arg := args[i]
if strings.HasPrefix(arg, flag) {
toks := strings.Split(arg, "=")
if len(toks) == 2 {
return toks[1], nil
} else if i+1 == len(args) {
return "", fmt.Errorf("flag needs an argument: %s", flag)
} else {
return args[i+1], nil
}
}
}
return "", fmt.Errorf("missing flag: %s", flag)
}
func parsePathFlag(args []string, flag string) (string, error) {
path, err := parseFlag(args, flag)
if err != nil {
return "", err
}
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("file `%s` does not exist", path)
}
return path, nil
}
package main
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseFlag(t *testing.T) {
cases := []struct {
name string
args string
flag string
expect string
expectErr string
}{
{
name: "bar=one",
args: "--foo --bar=one --baz",
flag: "--bar",
expect: "one",
},
{
name: "bar one",
args: "--foo --bar one --baz",
flag: "--bar",
expect: "one",
},
{
name: "bar one first flag",
args: "--bar one --foo two --baz three",
flag: "--bar",
expect: "one",
},
{
name: "bar one last flag",
args: "--foo --baz --bar one",
flag: "--bar",
expect: "one",
},
{
name: "non-existent flag",
args: "--foo one",
flag: "--bar",
expectErr: "missing flag",
},
{
name: "empty args",
args: "",
flag: "--foo",
expectErr: "missing flag",
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
args := strings.Split(tt.args, " ")
result, err := parseFlag(args, tt.flag)
if tt.expectErr != "" {
require.ErrorContains(t, err, tt.expectErr)
} else {
require.NoError(t, err)
require.Equal(t, tt.expect, result)
}
})
}
}
package main
import (
"fmt"
"os"
"github.com/urfave/cli/v2"
"github.com/ethereum-optimism/optimism/cannon/cmd"
"github.com/ethereum-optimism/optimism/cannon/mipsevm/versions"
)
func Witness(ctx *cli.Context) error {
if len(os.Args) == 3 && os.Args[2] == "--help" {
if err := list(); err != nil {
return err
}
fmt.Println("use `--input <valid input file> --help` to get more detailed help")
}
inputPath, err := parsePathFlag(os.Args[1:], "--input")
if err != nil {
return err
}
version, err := versions.DetectVersion(inputPath)
if err != nil {
return err
}
return ExecuteCannon(ctx.Context, os.Args[1:], version)
}
var WitnessCommand = cmd.CreateWitnessCommand(Witness)
......@@ -35,8 +35,8 @@ RUN --mount=type=cache,target=/root/.cache/go-build cd op-program && make op-pro
GOOS=linux GOARCH=mips GOMIPS=softfloat GITCOMMIT=$GIT_COMMIT GITDATE=$GIT_DATE VERSION="$OP_PROGRAM_VERSION"
# Run the op-program-client.elf binary directly through cannon's load-elf subcommand.
RUN /app/cannon/bin/cannon load-elf --path /app/op-program/bin/op-program-client.elf --out /app/op-program/bin/prestate.json --meta ""
RUN /app/cannon/bin/cannon load-elf --type cannon-mt --path /app/op-program/bin/op-program-client.elf --out /app/op-program/bin/prestate-mt.bin.gz --meta ""
RUN /app/cannon/bin/cannon load-elf --type singlethreaded --path /app/op-program/bin/op-program-client.elf --out /app/op-program/bin/prestate.json --meta ""
RUN /app/cannon/bin/cannon load-elf --type multithreaded --path /app/op-program/bin/op-program-client.elf --out /app/op-program/bin/prestate-mt.bin.gz --meta ""
# Generate the prestate proof containing the absolute pre-state hash.
RUN /app/cannon/bin/cannon run --proof-at '=0' --stop-at '=1' --input /app/op-program/bin/prestate.json --meta "" --proof-fmt '/app/op-program/bin/%d.json' --output ""
......
......@@ -46,8 +46,16 @@ ARG TARGETARCH
# Build the Go services, utilizing caches and share the many common packages.
# The "id" defaults to the value of "target", the cache will thus be reused during this build.
# "sharing" defaults to "shared", the cache will thus be available to other concurrent docker builds.
# For now fetch the v1 cannon binary from the op-challenger image
#FROM --platform=$BUILDPLATFORM us-docker.pkg.dev/oplabs-tools-artifacts/images/op-challenger:v1.1.0 AS cannon-builder-0
FROM --platform=$BUILDPLATFORM builder AS cannon-builder
ARG CANNON_VERSION=v0.0.0
# note: bump this CANNON_VERSION when the VM behavior changes
ARG CANNON_VERSION=v1.0.0
# uncomment these lines once there's a new Cannon version available
#COPY --from=cannon-builder-0 /usr/local/bin/cannon ./cannon/multicannon/embeds/cannon-0
#COPY --from=cannon-builder-0 /usr/local/bin/cannon ./cannon/multicannon/embeds/cannon-1
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build cd cannon && make cannon \
GOOS=$TARGETOS GOARCH=$TARGETARCH GITCOMMIT=$GIT_COMMIT GITDATE=$GIT_DATE VERSION="$CANNON_VERSION"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment