Skip to content

Commit

Permalink
common/compiler: simplify solc wrapper
Browse files Browse the repository at this point in the history
Support for legacy version 0.9.x is gone. The compiler version is no
longer cached. Compilation results (and the version) are read directly
from stdout using the --combined-json flag. As a workaround for
ethereum/solidity#651, source code is written to a temporary file before
compilation.

Integration of solc in package ethapi and cmd/abigen is now much simpler
because the compiler wrapper is no longer passed around as a pointer.

Fixes ethereum#2806, accidentally
  • Loading branch information
fjl committed Aug 17, 2016
1 parent 84d11c1 commit 1a9e669
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 254 deletions.
13 changes: 1 addition & 12 deletions cmd/abigen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,7 @@ func main() {
for _, kind := range strings.Split(*excFlag, ",") {
exclude[strings.ToLower(kind)] = true
}
// Build the Solidity source into bindable components
solc, err := compiler.New(*solcFlag)
if err != nil {
fmt.Printf("Failed to locate Solidity compiler: %v\n", err)
os.Exit(-1)
}
source, err := ioutil.ReadFile(*solFlag)
if err != nil {
fmt.Printf("Failed to read Soldity source code: %v\n", err)
os.Exit(-1)
}
contracts, err := solc.Compile(string(source))
contracts, err := compiler.CompileSolidity(*solcFlag, *solFlag)
if err != nil {
fmt.Printf("Failed to build Solidity contract: %v\n", err)
os.Exit(-1)
Expand Down
215 changes: 93 additions & 122 deletions common/compiler/solidity.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,31 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

// Package compiler wraps the Solidity compiler executable (solc).
package compiler

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
)

var (
versionRegexp = regexp.MustCompile("[0-9]+\\.[0-9]+\\.[0-9]+")
legacyRegexp = regexp.MustCompile("0\\.(9\\..*|1\\.[01])")
paramsLegacy = []string{
"--binary", // Request to output the contract in binary (hexadecimal).
"file", //
"--json-abi", // Request to output the contract's JSON ABI interface.
"file", //
"--natspec-user", // Request to output the contract's Natspec user documentation.
"file", //
"--natspec-dev", // Request to output the contract's Natspec developer documentation.
"file",
"--add-std",
"1",
}
paramsNew = []string{
"--bin", // Request to output the contract in binary (hexadecimal).
"--abi", // Request to output the contract's JSON ABI interface.
"--userdoc", // Request to output the contract's Natspec user documentation.
"--devdoc", // Request to output the contract's Natspec developer documentation.
solcParams = []string{
"--combined-json", "bin,abi,userdoc,devdoc",
"--add-std", // include standard lib contracts
"--optimize", // code optimizer switched on
"-o", // output directory
}
)

Expand All @@ -76,135 +58,112 @@ type ContractInfo struct {
DeveloperDoc interface{} `json:"developerDoc"`
}

// Solidity contains information about the solidity compiler.
type Solidity struct {
solcPath string
version string
fullVersion string
legacy bool
Path, Version, FullVersion string
}

func New(solcPath string) (sol *Solidity, err error) {
// set default solc
if len(solcPath) == 0 {
solcPath = "solc"
}
solcPath, err = exec.LookPath(solcPath)
if err != nil {
return
}
// --combined-output format
type solcOutput struct {
Contracts map[string]struct{ Bin, Abi, Devdoc, Userdoc string }
Version string
}

cmd := exec.Command(solcPath, "--version")
// SolidityVersion runs solc and parses its version output.
func SolidityVersion(solc string) (*Solidity, error) {
if solc == "" {
solc = "solc"
}
var out bytes.Buffer
cmd := exec.Command(solc, "--version")
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
return
if err := cmd.Run(); err != nil {
return nil, err
}

fullVersion := out.String()
version := versionRegexp.FindString(fullVersion)
legacy := legacyRegexp.MatchString(version)

sol = &Solidity{
solcPath: solcPath,
version: version,
fullVersion: fullVersion,
legacy: legacy,
s := &Solidity{
Path: cmd.Path,
FullVersion: out.String(),
Version: versionRegexp.FindString(out.String()),
}
glog.V(logger.Info).Infoln(sol.Info())
return
}

func (sol *Solidity) Info() string {
return fmt.Sprintf("%s\npath: %s", sol.fullVersion, sol.solcPath)
}

func (sol *Solidity) Version() string {
return sol.version
return s, nil
}

// Compile builds and returns all the contracts contained within a source string.
func (sol *Solidity) Compile(source string) (map[string]*Contract, error) {
// Short circuit if no source code was specified
// 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")
}
// Create a safe place to dump compilation output
wd, err := ioutil.TempDir("", "solc")
if solc == "" {
solc = "solc"
}
// Write source to a temporary file. Compiling stdin used to be supported
// but seems to produce an exception with solc 0.3.5.
infile, err := ioutil.TempFile("", "geth-compile-solidity")
if err != nil {
return nil, fmt.Errorf("solc: failed to create temporary build folder: %v", err)
return nil, err
}
defer os.Remove(infile.Name())
if _, err := io.WriteString(infile, source); err != nil {
return nil, err
}
if err := infile.Close(); err != nil {
return nil, err
}
defer os.RemoveAll(wd)

// Assemble the compiler command, change to the temp folder and capture any errors
stderr := new(bytes.Buffer)
return CompileSolidity(solc, infile.Name())
}

var params []string
if sol.legacy {
params = paramsLegacy
} else {
params = paramsNew
params = append(params, wd)
// 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 ")
}
source, err := slurpFiles(sourcefiles)
if err != nil {
return nil, err
}
if solc == "" {
solc = "solc"
}
compilerOptions := strings.Join(params, " ")

cmd := exec.Command(sol.solcPath, params...)
cmd.Stdin = strings.NewReader(source)
cmd.Stderr = stderr

var stderr, stdout bytes.Buffer
args := append(solcParams, "--")
cmd := exec.Command(solc, append(args, sourcefiles...)...)
cmd.Stderr = &stderr
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("solc: %v\n%s", err, string(stderr.Bytes()))
return nil, fmt.Errorf("solc: %v\n%s", err, stderr.Bytes())
}
// Sanity check that something was actually built
matches, _ := filepath.Glob(filepath.Join(wd, "*.bin*"))
if len(matches) < 1 {
return nil, fmt.Errorf("solc: no build results found")
var output solcOutput
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
return nil, err
}
// Compilation succeeded, assemble and return the contracts
contracts := make(map[string]*Contract)
for _, path := range matches {
_, file := filepath.Split(path)
base := strings.Split(file, ".")[0]

// Parse the individual compilation results (code binary, ABI definitions, user and dev docs)
var binary []byte
binext := ".bin"
if sol.legacy {
binext = ".binary"
}
if binary, err = ioutil.ReadFile(filepath.Join(wd, base+binext)); err != nil {
return nil, fmt.Errorf("solc: error reading compiler output for code: %v", err)
}
shortVersion := versionRegexp.FindString(output.Version)

// 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 blob, err := ioutil.ReadFile(filepath.Join(wd, base+".abi")); err != nil {
return nil, fmt.Errorf("solc: error reading abi definition: %v", err)
} else if err = json.Unmarshal(blob, &abi); err != nil {
return nil, fmt.Errorf("solc: error parsing abi definition: %v", err)
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 blob, err := ioutil.ReadFile(filepath.Join(wd, base+".docuser")); err != nil {
if err := json.Unmarshal([]byte(info.Userdoc), &userdoc); err != nil {
return nil, fmt.Errorf("solc: error reading user doc: %v", err)
} else if err = json.Unmarshal(blob, &userdoc); err != nil {
return nil, fmt.Errorf("solc: error parsing user doc: %v", err)
}

var devdoc interface{}
if blob, err := ioutil.ReadFile(filepath.Join(wd, base+".docdev")); err != nil {
if err := json.Unmarshal([]byte(info.Devdoc), &devdoc); err != nil {
return nil, fmt.Errorf("solc: error reading dev doc: %v", err)
} else if err = json.Unmarshal(blob, &devdoc); err != nil {
return nil, fmt.Errorf("solc: error parsing dev doc: %v", err)
}
// Assemble the final contract
contracts[base] = &Contract{
Code: "0x" + string(binary),
contracts[name] = &Contract{
Code: "0x" + info.Bin,
Info: ContractInfo{
Source: source,
Language: "Solidity",
LanguageVersion: sol.version,
CompilerVersion: sol.version,
CompilerOptions: compilerOptions,
LanguageVersion: shortVersion,
CompilerVersion: shortVersion,
CompilerOptions: strings.Join(solcParams, " "),
AbiDefinition: abi,
UserDoc: userdoc,
DeveloperDoc: devdoc,
Expand All @@ -214,12 +173,24 @@ func (sol *Solidity) Compile(source string) (map[string]*Contract, error) {
return contracts, nil
}

func SaveInfo(info *ContractInfo, filename string) (contenthash common.Hash, err error) {
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)
}
return concat.String(), nil
}

// SaveInfo serializes info to the given file and returns its Keccak256 hash.
func SaveInfo(info *ContractInfo, filename string) (common.Hash, error) {
infojson, err := json.Marshal(info)
if err != nil {
return
return common.Hash{}, err
}
contenthash = common.BytesToHash(crypto.Keccak256(infojson))
err = ioutil.WriteFile(filename, infojson, 0600)
return
contenthash := common.BytesToHash(crypto.Keccak256(infojson))
return contenthash, ioutil.WriteFile(filename, infojson, 0600)
}
Loading

0 comments on commit 1a9e669

Please sign in to comment.