diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go index 3a1ae6f4c3..6bc6b516f7 100644 --- a/cmd/abigen/main.go +++ b/cmd/abigen/main.go @@ -17,7 +17,6 @@ package main import ( - "encoding/json" "flag" "fmt" "io/ioutil" @@ -81,19 +80,21 @@ func main() { for _, kind := range strings.Split(*excFlag, ",") { exclude[strings.ToLower(kind)] = true } - contracts, err := compiler.CompileSolidity(*solcFlag, *solFlag) - if err != nil { + solc, err := compiler.InitSolc(*solcFlag) + + solReturn, err := solc.Compile(compiler.FlagOpts{}, *solFlag) + if err != nil || solReturn.Typ != compiler.Solc { fmt.Printf("Failed to build Solidity contract: %v\n", err) os.Exit(-1) } // Gather all non-excluded contract for binding - for name, contract := range contracts { + for name, contract := range solReturn.Contracts { if exclude[strings.ToLower(name)] { continue } - abi, _ := json.Marshal(contract.Info.AbiDefinition) // Flatten the compiler parse - abis = append(abis, string(abi)) - bins = append(bins, contract.Code) + + abis = append(abis, contract.Abi) + bins = append(bins, contract.Bin) nameParts := strings.Split(name, ":") types = append(types, nameParts[len(nameParts)-1]) diff --git a/common/compiler/solidity.go b/common/compiler/solidity.go index abb8039896..dc56d187e2 100644 --- a/common/compiler/solidity.go +++ b/common/compiler/solidity.go @@ -22,174 +22,207 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "os/exec" + "reflect" "regexp" "strconv" "strings" + + "github.com/ethereum/go-ethereum/common" ) var versionRegexp = regexp.MustCompile(`([0-9]+)\.([0-9]+)\.([0-9]+)`) -type Contract struct { - Code string `json:"code"` - Info ContractInfo `json:"info"` +// Initialize a versioned Solc compiler. +func InitSolc(command string) (*Solidity, error) { + if command == "" { + command = "solc" + } + if _, err := exec.LookPath(command); err != nil { + return nil, fmt.Errorf("compiler: could not find %v in PATH", command) + } + s := &Solidity{NamedCmd: command} + if err := s.version(); err != nil { + return nil, err + } + return s, nil + } -type ContractInfo struct { - Source string `json:"source"` - Language string `json:"language"` - LanguageVersion string `json:"languageVersion"` - CompilerVersion string `json:"compilerVersion"` - CompilerOptions string `json:"compilerOptions"` - AbiDefinition interface{} `json:"abiDefinition"` - UserDoc interface{} `json:"userDoc"` - DeveloperDoc interface{} `json:"developerDoc"` - Metadata string `json:"metadata"` +//The following represents solidity outputs from the compiler that we're interested in +type SolcReturn struct { + Warning string + Version string `json:"version"` + Contracts map[string]SolcItems `json:"contracts"` +} + +//The key return items to enable unmarshalling from the returns from the compiler +type SolcItems struct { + Bin string `json:"bin"` + Abi string `json:"abi"` + DevDoc string `json:"devdoc"` + UserDoc string `json:"userdoc"` + Metadata string `json:"metadata"` } // Solidity contains information about the solidity compiler. type Solidity struct { - Path, Version, FullVersion string - Major, Minor, Patch int + NamedCmd, Path, Version, FullVersion string + Major, Minor, Patch int } -// --combined-output format -type solcOutput struct { - Contracts map[string]struct { - Bin, Abi, Devdoc, Userdoc, Metadata string - } - Version string -} - -func (s *Solidity) makeArgs() []string { - p := []string{ - "--combined-json", "bin,abi,userdoc,devdoc", - "--add-std", // include standard lib contracts - "--optimize", // code optimizer switched on - } - if s.Major > 0 || s.Minor > 4 || s.Patch > 6 { - p[1] += ",metadata" - } - return p +//This is a template to define our inputs for the compiler flags +type SolcFlagOpts struct { + // (Required) what to get in the output, can be any combination of [abi, bin, userdoc, devdoc, metadata] + // abi: application binary interface. Necessary for interaction with contracts. + // bin: binary bytecode. Necessary for creating and deploying and interacting with contracts. + // userdoc: natspec for users. + // devdoc: natspec for devs. + // metadata: contract metadata. + CombinedOutput []string + // (Optional) Direct string of library address mappings. + // Syntax: :
,:
+ // Address is interpreted as a hex string optionally prefixed by 0x. + Libraries map[string]common.Address + // (Optional) Remappings, see https://solidity.readthedocs.io/en/latest/layout-of-source-files.html#use-in-actual-compilers + // Syntax: = + Remappings []string + // (Optional) if true, enable standard library contracts + StdLib bool + // (Optional) if true, optimizes solidity code + Optimize bool + // (Optional) the number of optimization runs to run on solidity + OptimizeRuns uint64 + // (Optional) For anything else we may have missed, if filled will default override other flags. + Exec string } // SolidityVersion runs solc and parses its version output. -func SolidityVersion(solc string) (*Solidity, error) { - if solc == "" { - solc = "solc" - } +func (s *Solidity) version() error { var out bytes.Buffer - cmd := exec.Command(solc, "--version") + cmd := exec.Command(s.NamedCmd, "--version") cmd.Stdout = &out err := cmd.Run() if err != nil { - return nil, err + return err } matches := versionRegexp.FindStringSubmatch(out.String()) if len(matches) != 4 { - return nil, fmt.Errorf("can't parse solc version %q", out.String()) + return fmt.Errorf("%v: can't parse version %q", s.NamedCmd, out.String()) } - s := &Solidity{Path: cmd.Path, FullVersion: out.String(), Version: matches[0]} + s = &Solidity{Path: cmd.Path, FullVersion: out.String(), Version: matches[0]} if s.Major, err = strconv.Atoi(matches[1]); err != nil { - return nil, err + return err } if s.Minor, err = strconv.Atoi(matches[2]); err != nil { - return nil, err + return err } if s.Patch, err = strconv.Atoi(matches[3]); err != nil { - return nil, err + return err } - return s, nil + return nil } -// CompileSolidityString builds and returns all the contracts contained within a source string. -func CompileSolidityString(solc, source string) (map[string]*Contract, error) { - if len(source) == 0 { - return nil, errors.New("solc: empty source string") +// Compiles a series of files using the solidity compiler +func (s *Solidity) Compile(flags SolcFlagOpts, files ...string) (Return, error) { + + if reflect.DeepEqual(flags.SolcFlagOpts, (SolcFlagOpts{})) { + flags.defaultSolcFlagOpts(s) } - s, err := SolidityVersion(solc) - if err != nil { - return nil, err - } - args := append(s.makeArgs(), "--") - cmd := exec.Command(s.Path, append(args, "-")...) - cmd.Stdin = strings.NewReader(source) - return s.run(cmd, source) + + return s.execute(flags.assembleSolcCommand(files...)...) } -// CompileSolidity compiles all given Solidity source files. -func CompileSolidity(solc string, sourcefiles ...string) (map[string]*Contract, error) { - if len(sourcefiles) == 0 { - return nil, errors.New("solc: no source files") +func (s *Solidity) Link(libs map[string]common.Address, binary string) (string, error) { + var refinedBinary bytes.Buffer + var stderr bytes.Buffer + + buf := bytes.NewBufferString(binary) + linkCmd := exec.Command("solc", "--link", "--libraries", stringifyLibs(libs)) + + linkCmd.Stdin = buf + linkCmd.Stderr = &stderr + linkCmd.Stdout = &refinedBinary + + linkCmd.Start() + linkCmd.Wait() + + if stderr.String() != "" { + return "", errors.New(stderr.String()) } - source, err := slurpFiles(sourcefiles) - if err != nil { - return nil, err - } - s, err := SolidityVersion(solc) - if err != nil { - return nil, err - } - args := append(s.makeArgs(), "--") - cmd := exec.Command(s.Path, append(args, sourcefiles...)...) - return s.run(cmd, source) + + return refinedBinary.String(), nil } -func (s *Solidity) run(cmd *exec.Cmd, source string) (map[string]*Contract, error) { +func stringifyLibs(libs map[string]common.Address) string { + var combinedLibs []string + for x, y := range libs { + combinedLibs = append(combinedLibs, x+":"+y.String()) + } + return strings.Join(combinedLibs, ",") +} + +func (f SolcFlagOpts) assembleSolcCommand(files ...string) (command []string) { + + switch { + case f.Exec != "": + command = append(command, f.Exec) + default: + if len(f.Remappings) > 0 { + command = append(command, strings.Join(f.Remappings, " ")) + } + if len(f.CombinedOutput) > 0 { + command = append(command, "--combined-json", strings.Join(f.CombinedOutput, ",")) + } + if len(f.Libraries) > 0 { + command = append(command, []string{"--libraries", stringifyLibs(f.Libraries)}...) + } + if f.Optimize { + command = append(command, "--optimize") + } + if f.StdLib { + command = append(command, "--std-lib") + } + if f.OptimizeRuns != 0 { + command = append(command, "--optimize-runs", strconv.FormatUint(f.OptimizeRuns, 10)) + } + } + return append(command, files...) +} + +func (f *SolcFlagOpts) defaultSolcFlagOpts(s *Solidity) { + f = &SolcFlagOpts{ + CombinedOutput: []string{"bin", "abi", "userdoc", "devdoc"}, + StdLib: true, + Optimize: true, + } + + if s.Major >= 0 && s.Minor >= 4 && s.Patch > 6 { + f.CombinedOutput = append(f.CombinedOutput, "metadata") + } + + return +} + +func (s *Solidity) execute(flagsAndFiles ...string) (SolcReturn, error) { var stderr, stdout bytes.Buffer + var output SolcReturn + + cmd := exec.Command(s.NamedCmd, flagsAndFiles...) cmd.Stderr = &stderr cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("solc: %v\n%s", err, stderr.Bytes()) - } - var output solcOutput - if err := json.Unmarshal(stdout.Bytes(), &output); err != nil { - return nil, err + return Return{}, fmt.Errorf("%v: %v\n%s", s.NamedCmd, err, stderr.Bytes()) } - // Compilation succeeded, assemble and return the contracts. - contracts := make(map[string]*Contract) - for name, info := range output.Contracts { - // Parse the individual compilation results. - var abi interface{} - if err := json.Unmarshal([]byte(info.Abi), &abi); err != nil { - return nil, fmt.Errorf("solc: error reading abi definition (%v)", err) - } - var userdoc interface{} - if err := json.Unmarshal([]byte(info.Userdoc), &userdoc); err != nil { - return nil, fmt.Errorf("solc: error reading user doc: %v", err) - } - var devdoc interface{} - if err := json.Unmarshal([]byte(info.Devdoc), &devdoc); err != nil { - return nil, fmt.Errorf("solc: error reading dev doc: %v", err) - } - contracts[name] = &Contract{ - Code: "0x" + info.Bin, - Info: ContractInfo{ - Source: source, - Language: "Solidity", - LanguageVersion: s.Version, - CompilerVersion: s.Version, - CompilerOptions: strings.Join(s.makeArgs(), " "), - AbiDefinition: abi, - UserDoc: userdoc, - DeveloperDoc: devdoc, - Metadata: info.Metadata, - }, - } - } - return contracts, nil -} + buf := stdout.Bytes() -func slurpFiles(files []string) (string, error) { - var concat bytes.Buffer - for _, file := range files { - content, err := ioutil.ReadFile(file) - if err != nil { - return "", err - } - concat.Write(content) + if err := json.Unmarshal(buf, &output); err != nil { + return Return{}, err } - return concat.String(), nil + + output.Warning = string(stderr.Bytes()) + + return output, nil } diff --git a/common/compiler/solidity_test.go b/common/compiler/solidity_test.go index 0da3bb337e..5691a757f4 100644 --- a/common/compiler/solidity_test.go +++ b/common/compiler/solidity_test.go @@ -17,20 +17,110 @@ package compiler import ( + "io/ioutil" + "os" "os/exec" + "path/filepath" + "strings" "testing" + + "github.com/ethereum/go-ethereum/common" ) -const ( - testSource = ` -contract test { - /// @notice Will multiply ` + "`a`" + ` by 7. - function multiply(uint a) returns(uint d) { - return a * 7; - } +const regularSolFile = `pragma solidity >= 0.0.0; + contract main { + + }` + +const faultySol = `pragma solidity >= 0.0.0; + contract main { + uint a; + function f() { + a = 1; + } + ` +const solNoPragma = `contract main { + uint a; + function f() { + a = 1; + } + }` +const simplyLibrarySol = `pragma solidity >=0.0.0; + +library Set { + struct Data { mapping(uint => bool) flags; } + function insert(Data storage self, uint value) + returns (bool) + { + if (self.flags[value]) + return false; // already there + self.flags[value] = true; + return true; + } + + function remove(Data storage self, uint value) + returns (bool) + { + if (!self.flags[value]) + return false; // not there + self.flags[value] = false; + return true; + } + + function contains(Data storage self, uint value) + returns (bool) + { + return self.flags[value]; + } } -` -) + +contract C { + Set.Data knownValues; + function register(uint value) { + if (!Set.insert(knownValues, value)) + throw; + } +}` + +const solFile1 = `pragma solidity >=0.0.0; +import "/somedir/set.sol"; + +contract C { + Set.Data knownValues; + function register(uint value) { + if (!Set.insert(knownValues, value)) + throw; + } +}` + +const solFile2 = `pragma solidity >=0.0.0; + +library Set { + struct Data { mapping(uint => bool) flags; } + function insert(Data storage self, uint value) + returns (bool) + { + if (self.flags[value]) + return false; // already there + self.flags[value] = true; + return true; + } + + function remove(Data storage self, uint value) + returns (bool) + { + if (!self.flags[value]) + return false; // not there + self.flags[value] = false; + return true; + } + + function contains(Data storage self, uint value) + returns (bool) + { + return self.flags[value]; + } +}` func skipWithoutSolc(t *testing.T) { if _, err := exec.LookPath("solc"); err != nil { @@ -38,40 +128,213 @@ func skipWithoutSolc(t *testing.T) { } } -func TestCompiler(t *testing.T) { +func writeToTempFile(tmpfile *os.File, content []byte) error { + + if _, err := tmpfile.Write(content); err != nil { + return err + } + if err := tmpfile.Close(); err != nil { + return err + } + return nil +} + +func TestSolcCompilerNormal(t *testing.T) { + skipWithoutSolc(t) - contracts, err := CompileSolidityString("", testSource) + solc, err := InitSolc("solc") if err != nil { - t.Fatalf("error compiling source. result %v: %v", contracts, err) + t.Fatalf("Could not initialize solc: %v", err) } - if len(contracts) != 1 { - t.Errorf("one contract expected, got %d", len(contracts)) + + content := []byte(regularSolFile) + tmpfile, err := ioutil.TempFile("", "simpleContract.sol") + if err != nil { + t.Fatal(err) } - c, ok := contracts["test"] - if !ok { - c, ok = contracts[":test"] - if !ok { - t.Fatal("info for contract 'test' not present in result") - } + defer os.Remove(tmpfile.Name()) // clean up + + err = writeToTempFile(tmpfile, content) + if err != nil { + t.Fatal(err) } - if c.Code == "" { - t.Error("empty code") + + flags := SolcFlagOpts{ + CombinedOutput: []string{"bin", "abi"}, } - if c.Info.Source != testSource { - t.Error("wrong source") + + solReturn, err := solc.Compile(FlagOpts{flags}, tmpfile.Name()) + if err != nil { + t.Errorf("Expected no errors: %v", err) } - if c.Info.CompilerVersion == "" { - t.Error("empty version") + + if solReturn.Warning != "" || len(solReturn.Contracts) != 1 { + t.Fatalf("Expected no warnings and expected 1 contract item. Got %v for warnings, and %v for contract items", solReturn.Warning, len(solReturn.Contracts)) } } -func TestCompileError(t *testing.T) { +func TestSolcCompilerError(t *testing.T) { + skipWithoutSolc(t) - contracts, err := CompileSolidityString("", testSource[4:]) - if err == nil { - t.Errorf("error expected compiling source. got none. result %v", contracts) + solc, err := InitSolc("solc") + if err != nil { + t.Fatalf("Could not initialize solc: %v", err) + } + content := []byte(faultySol) + tmpfile, err := ioutil.TempFile("", "faultyContract.sol") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) // clean up + + err = writeToTempFile(tmpfile, content) + if err != nil { + t.Fatal(err) + } + + flags := SolcFlagOpts{ + CombinedOutput: []string{"bin", "abi"}, + } + + _, err = solc.Compile(FlagOpts{SolcFlagOpts: flags}, tmpfile.Name()) + + if err == nil { + t.Fatal("Expected an error, got nil.") + } else if !strings.Contains(err.Error(), "solc") { + t.Fatalf("Expected error to come directly from compiler, got err from elsewhere: %v", err) + } +} + +func TestSolcCompilerWarning(t *testing.T) { + + skipWithoutSolc(t) + + solc, err := InitSolc("solc") + if err != nil { + t.Fatalf("Could not initialize solc: %v", err) + } + content := []byte(solNoPragma) + tmpfile, err := ioutil.TempFile("", "warningContract.sol") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) // clean up + + err = writeToTempFile(tmpfile, content) + if err != nil { + t.Fatal(err) + } + + flags := SolcFlagOpts{ + CombinedOutput: []string{"bin", "abi"}, + } + + solReturn, err := solc.Compile(FlagOpts{SolcFlagOpts: flags}, tmpfile.Name()) + if err != nil { + t.Error(err) + } + if solReturn.Warning == "" { + t.Error("Expected a warning, got none.") + } +} + +func TestLinkingBinaries(t *testing.T) { + + skipWithoutSolc(t) + + solc, err := InitSolc("solc") + if err != nil { + t.Fatalf("Could not initialize solc: %v", err) + } + content := []byte(simplyLibrarySol) + tmpfile, err := ioutil.TempFile("", "libraryContracts.sol") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) // clean up + + err = writeToTempFile(tmpfile, content) + if err != nil { + t.Fatal(err) + } + + flags := SolcFlagOpts{ + CombinedOutput: []string{"bin"}, + } + + solReturn, err := solc.Compile(FlagOpts{SolcFlagOpts: flags}, tmpfile.Name()) + if err != nil { + t.Fatal(err) + } + + if solReturn.Warning != "" || len(solReturn.Contracts) != 2 { + t.Fatalf("Expected no errors or warnings and expected contract items. Got %v for warnings, and %v for contract items", solReturn.Warning, solReturn.Contracts) + } + + output, err := solc.(*Solidity).Link(map[string]common.Address{"Set": common.StringToAddress("0x692a70d2e424a56d2c6c27aa97d1a86395877b3a")}, solReturn.Contracts["C"].Bin) + if err != nil { + t.Error(err) + } + if strings.Contains(output, "_") { + t.Errorf("Expected binaries to link, but they did not") + } +} + +func TestRemappings(t *testing.T) { + + skipWithoutSolc(t) + + solc, err := InitSolc("solc") + if err != nil { + t.Fatalf("Could not initialize solc: %v", err) + } + + tmpfile1, err := ioutil.TempFile("", "C.sol") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile1.Name()) // clean up + + err = writeToTempFile(tmpfile1, []byte(solFile1)) + if err != nil { + t.Fatal(err) + } + + dir, err := ioutil.TempDir("", "tempDir") + if err != nil { + t.Fatal(err) + } + + defer os.RemoveAll(dir) // clean up + + tmpfn := filepath.Join(dir, "set.sol") + if err := ioutil.WriteFile(tmpfn, []byte(solFile2), 0666); err != nil { + t.Fatal(err) + } + + tmpfile2, err := ioutil.TempFile("", "main.sol") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile2.Name()) // clean up + + err = writeToTempFile(tmpfile2, []byte(regularSolFile)) + if err != nil { + t.Fatal(err) + } + flags := SolcFlagOpts{ + CombinedOutput: []string{"bin", "abi"}, + Remappings: []string{`/somedir/=` + dir + "/"}, + } + + solReturn, err := solc.Compile(FlagOpts{SolcFlagOpts: flags}, tmpfile1.Name(), tmpfile2.Name()) + if err != nil { + t.Fatal(err) + } + + if solReturn.Warning != "" || len(solReturn.Contracts) != 3 { + t.Fatalf("Expected no warnings and expected %v contract items. Got %v for warnings, and %v for contract items", 3, solReturn.Warning, len(solReturn.Contracts)) } - t.Logf("error: %v", err) }