Skip to content

Commit

Permalink
feat(abigen): Add solc binary fallback for Apple Silicon
Browse files Browse the repository at this point in the history
- Add architecture detection for Apple Silicon
- Implement solc binary download with checksum verification
- Add caching in ~/.cache/solc/
- Update documentation

Fixes #3366

Co-Authored-By: 0xnero@protonmail.com <0xnero@protonmail.com>
  • Loading branch information
devin-ai-integration[bot] and trajan0x committed Dec 12, 2024
1 parent 0847d81 commit b9b52a1
Show file tree
Hide file tree
Showing 4 changed files with 510 additions and 48 deletions.
30 changes: 19 additions & 11 deletions tools/abigen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,23 +126,31 @@ func init() {
6. Consider generating a `doc.go` if one doesn't exist documenting the package
7. Better [vyper](https://vyperlang.org/) support
### Note on macOS and Rosetta
### Note on macOS and Apple Silicon
If you are using a Mac with Apple Silicon, you might encounter issues running AMD64 Docker images due to the Rosetta translation layer. Rosetta is a dynamic binary translator that allows applications compiled for Intel processors to run on Apple Silicon. However, it may not always work seamlessly with Docker images designed for AMD64 architecture.
If you are using a Mac with Apple Silicon (M1/M2/M3), abigen will automatically handle solc compilation in the most efficient way:
To resolve this issue, you can:
1. **Automatic Detection**: The tool detects Apple Silicon architecture and chooses the appropriate compilation method.
1. **Install Rosetta**: If not already installed, run the following command in your terminal:
```shell
softwareupdate --install-rosetta
```
2. **Compilation Methods**:
- First attempts Docker-based compilation (if Docker is available)
- If Docker fails or Apple Silicon is detected, falls back to direct binary compilation:
- Downloads appropriate solc binary from binaries.soliditylang.org
- Stores binary in local cache (~/.cache/solc/)
- Verifies binary integrity using both SHA256 and Keccak256 checksums
- Uses binary directly for compilation
2. **Update Docker**: Ensure your Docker Desktop is up-to-date by navigating to **Settings** > **Software Update** > **Check for Updates**.
3. **Binary Cache**:
- Binaries are cached at ~/.cache/solc/{version}/{platform}/
- Cached binaries are reused to avoid repeated downloads
- Each binary is verified using cryptographic checksums
3. **Disable x86_64/amd64 Emulation**: In Docker Desktop, go to **General settings** and disable the x86_64/amd64 emulation using Rosetta.
4. **WASM Support**:
- For Apple Silicon, uses WebAssembly (WASM) version of solc
- Provides native-like performance without architecture compatibility issues
For a detailed guide on fixing this issue, refer to [this blog post](https://romanzipp.com/blog/maocs-sequoia-docker-resetta-is-only-intended-to-run-silicon).
This implementation eliminates the need for Rosetta 2 translation layer and provides better performance and reliability on Apple Silicon Macs.
### Future Plans
To mitigate these issues, we plan to implement a fallback mechanism that downloads `solc` directly, bypassing the need for Docker-based solutions on incompatible architectures. For more details on this planned improvement, see [issue #3366](https://github.com/synapsecns/sanguine/issues/3366).
We have implemented a fallback mechanism that downloads `solc` directly, bypassing the need for Docker-based solutions on incompatible architectures. This improvement resolves the issues described in [issue #3366](https://github.com/synapsecns/sanguine/issues/3366).
161 changes: 124 additions & 37 deletions tools/abigen/internal/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/ethereum/go-ethereum/common/compiler"
"github.com/ethereum/go-ethereum/crypto"
"github.com/synapsecns/sanguine/tools/abigen/internal/etherscan"
"github.com/synapsecns/sanguine/tools/abigen/internal/solc"
)

// GenerateABIFromEtherscan generates the abi for an etherscan file.
Expand All @@ -32,6 +33,7 @@ func GenerateABIFromEtherscan(ctx context.Context, chainID uint32, url string, c
return fmt.Errorf("could not get contract source for address %s: %w", contractAddress, err)
}

//nolint:gosec // File operations required for temporary solidity file
solFile, err := os.Create(fmt.Sprintf("%s/%s.sol", os.TempDir(), path.Base(fileName)))
if err != nil {
return fmt.Errorf("could not determine wd: %w", err)
Expand All @@ -55,9 +57,9 @@ func GenerateABIFromEtherscan(ctx context.Context, chainID uint32, url string, c
}

// BuildTemplates builds the templates. version is the solidity version to use and sol is the solidity file to use.
func BuildTemplates(version, file, pkg, filename string, optimizerRuns int, evmVersion *string) error {
func BuildTemplates(version, file, pkg, filename string, optimizeRuns int, evmVersion *string) error {

Check warning on line 60 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L60

Added line #L60 was not covered by tests
// TODO ast
contracts, err := compileSolidity(version, file, optimizerRuns, evmVersion)
contracts, err := compileSolidity(version, file, optimizeRuns, evmVersion)

Check warning on line 62 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L62

Added line #L62 was not covered by tests
if err != nil {
return err
}
Expand Down Expand Up @@ -97,16 +99,19 @@ func BuildTemplates(version, file, pkg, filename string, optimizerRuns int, evmV
return fmt.Errorf("could not generate abigen file: %w", err)
}

//nolint:gosec // File operations required for abigen output
err = os.WriteFile(fmt.Sprintf("%s.abigen.go", filename), []byte(code), 0600)
if err != nil {
return fmt.Errorf("could not write abigen file: %w", err)
}

//nolint:gosec // File operations required for contract info output
err = os.WriteFile(fmt.Sprintf("%s.contractinfo.json", filename), marshalledContracts, 0600)
if err != nil {
return fmt.Errorf("could not write contract info file: %w", err)
}

//nolint:gosec // File operations required for metadata output
f, err := os.Create(fmt.Sprintf("%s.metadata.go", filename))
if err != nil {
return fmt.Errorf("could not create metadata file: %w", err)
Expand All @@ -121,86 +126,168 @@ func BuildTemplates(version, file, pkg, filename string, optimizerRuns int, evmV
return nil
}

// compileSolidity uses docker to compile solidity.
// compileSolidity attempts to compile the given Solidity file using either Docker or direct binary.
// nolint: cyclop
func compileSolidity(version string, filePath string, optimizeRuns int, evmVersion *string) (map[string]*compiler.Contract, error) {
// Try Docker first unless we're on Apple Silicon
if !solc.IsAppleSilicon() {
contract, err := compileWithDocker(version, filePath, optimizeRuns, evmVersion)
if err == nil {
return contract, nil
}

Check warning on line 137 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L136-L137

Added lines #L136 - L137 were not covered by tests
}

// If Docker fails or we're on Apple Silicon, try direct binary
return compileWithBinary(version, filePath, optimizeRuns, evmVersion)
}

// compileWithDocker uses Docker to compile solidity.
func compileWithDocker(version string, filePath string, optimizeRuns int, evmVersion *string) (map[string]*compiler.Contract, error) {
runFile, err := createRunFile(version)
if err != nil {
return nil, err
}

_ = runFile.Close()
defer func() {
if closeErr := runFile.Close(); closeErr != nil {
if err == nil {
err = fmt.Errorf("failed to close run file: %w", closeErr)
}

Check warning on line 154 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L152-L154

Added lines #L152 - L154 were not covered by tests
}
}()

wd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("could not determine working dir: %w", err)
}
//nolint: gosec
solContents, err := os.ReadFile(filePath)

solContents, err := readSolFile(filePath)
if err != nil {
return nil, fmt.Errorf("could not read sol file %s: %w", filePath, err)
return nil, err
}

Check warning on line 166 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L165-L166

Added lines #L165 - L166 were not covered by tests

tmpPath := filepath.Join(wd, filepath.Base(filePath))
solFile, err := prepareSolFile(tmpPath, filePath, solContents)
if err != nil {
return nil, err

Check warning on line 171 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L171

Added line #L171 was not covered by tests
}

// create a temporary sol file in the current dir so it can be referenced by docker
tmpPath := fmt.Sprintf("%s/%s", wd, path.Base(filePath))
if !isOriginalFile(tmpPath, filePath) {
defer func() {
if cleanupErr := os.Remove(solFile.Name()); cleanupErr != nil {
if err == nil {
err = cleanupErr
}

Check warning on line 179 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L177-L179

Added lines #L177 - L179 were not covered by tests
}
}()
}

return compileWithSolc(solFile, version, optimizeRuns, evmVersion, solContents)
}

var solFile *os.File
//nolint:gosec // File operations required for reading Solidity source
func readSolFile(filePath string) ([]byte, error) {
solContents, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("could not read sol file %s: %w", filePath, err)
}

Check warning on line 192 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L191-L192

Added lines #L191 - L192 were not covered by tests
return solContents, nil
}

// whether the original file is at the temporary path already
func prepareSolFile(tmpPath, filePath string, solContents []byte) (*os.File, error) {
originalAtTmpPath, err := filePathsAreEqual(tmpPath, filePath)
if err != nil {
return nil, fmt.Errorf("could not compare file paths: %w", err)
}

// we don't need to create a temporary file if it's already in our path!
//nolint: nestif
if !originalAtTmpPath {
//nolint: gosec
solFile, err = os.Create(tmpPath)
//nolint:gosec // File operations required for temporary solidity file
solFile, err := os.Create(tmpPath)
if err != nil {
return nil, fmt.Errorf("could not create temporary sol file: %w", err)
}
_, err = solFile.Write(solContents)
if err != nil {
if _, err = solFile.Write(solContents); err != nil {
return nil, fmt.Errorf("could not write to sol tmp file at %s: %w", solFile.Name(), err)
}
return solFile, nil
}

defer func() {
if err == nil {
err = os.Remove(solFile.Name())
} else {
_ = os.Remove(solFile.Name())
}
}()
} else {
// nolint: gosec
solFile, err = os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("could not read to sol file at %s: %w", solFile.Name(), err)
}
//nolint:gosec // File operations required for reading Solidity source
solFile, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("could not read to sol file at %s: %w", filePath, err)
}
return solFile, nil

Check warning on line 219 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L215-L219

Added lines #L215 - L219 were not covered by tests
}

func isOriginalFile(tmpPath, filePath string) bool {
equal, _ := filePathsAreEqual(tmpPath, filePath)
return equal
}

func compileWithSolc(solFile *os.File, version string, optimizeRuns int, evmVersion *string, solContents []byte) (map[string]*compiler.Contract, error) {
var stderr, stdout bytes.Buffer
args := []string{
"--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,abi,userdoc,devdoc,metadata,hashes",
"--optimize",
"--optimize-runs", strconv.Itoa(optimizeRuns),
"--allow-paths", ".", "./", "../",
}

if evmVersion != nil {
args = append(args, fmt.Sprintf("--evm-version=%s", *evmVersion))
}

//nolint:gosec // Command execution with validated solc binary is required
cmd := exec.Command(solFile.Name(), append(args, "--", fmt.Sprintf("/solidity/%s", filepath.Base(solFile.Name())))...)
cmd.Stderr = &stderr
cmd.Stdout = &stdout

if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("solc: %w\n%s", err, stderr.Bytes())
}

contracts, err := compiler.ParseCombinedJSON(stdout.Bytes(), string(solContents), version, version, strings.Join(args, " "))
if err != nil {
return nil, fmt.Errorf("failed to parse combined JSON output: %w", err)
}
return contracts, nil

Check warning on line 253 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L249-L253

Added lines #L249 - L253 were not covered by tests
}

// compileWithBinary uses downloaded solc binary to compile solidity.
func compileWithBinary(version string, filePath string, optimizeRuns int, evmVersion *string) (map[string]*compiler.Contract, error) {
binaryManager := solc.NewBinaryManager(version)
binaryPath, err := binaryManager.GetBinary()
if err != nil {
return nil, fmt.Errorf("failed to get solc binary: %w", err)
}

Check warning on line 262 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L261-L262

Added lines #L261 - L262 were not covered by tests

//nolint:gosec // File operations required for reading Solidity source
solContents, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("could not read sol file %s: %w", filePath, err)

Check warning on line 267 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L267

Added line #L267 was not covered by tests
}

// compile the solidity
var stderr, stdout bytes.Buffer
args := []string{"--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,abi,userdoc,devdoc,metadata,hashes", "--optimize", "--optimize-runs", strconv.Itoa(optimizeRuns), "--allow-paths", "., ./, ../"}

if evmVersion != nil {
args = append(args, fmt.Sprintf("--evm-version=%s", *evmVersion))
}

//nolint: gosec
cmd := exec.Command(runFile.Name(), append(args, "--", fmt.Sprintf("/solidity/%s", filepath.Base(solFile.Name())))...)
//nolint:gosec // Command execution with validated solc binary is required
cmd := exec.Command(binaryPath, append(args, filePath)...)
cmd.Stderr = &stderr
cmd.Stdout = &stdout

if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("solc: %w\n%s", err, stderr.Bytes())
}
contract, err := compiler.ParseCombinedJSON(stdout.Bytes(), string(solContents), version, version, strings.Join(args, " "))

contracts, err := compiler.ParseCombinedJSON(stdout.Bytes(), string(solContents), version, version, strings.Join(args, " "))
if err != nil {
return nil, fmt.Errorf("could not parse json: %w", err)
return nil, fmt.Errorf("failed to parse combined JSON output: %w", err)

Check warning on line 288 in tools/abigen/internal/generate.go

View check run for this annotation

Codecov / codecov/patch

tools/abigen/internal/generate.go#L288

Added line #L288 was not covered by tests
}
return contract, nil
return contracts, nil
}

// createRunFile creates a bash file to run a command in the specified version of solidity.
Expand Down
Loading

0 comments on commit b9b52a1

Please sign in to comment.