diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 2d576d4..b744c3b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -24,7 +24,7 @@ jobs: python-version: 3.6 - name: Install Tox - run: pip install tox + run: pip install tox wheel - name: Run Tox run: tox -e lint @@ -36,13 +36,20 @@ jobs: steps: - uses: actions/checkout@v1 + # install solc for one job to test importing already-installed versions + - name: Install solc + run: | + sudo add-apt-repository ppa:ethereum/ethereum + sudo apt-get update + sudo apt-get install solc + - name: Setup Python 3.6 uses: actions/setup-python@v1 with: python-version: 3.6 - name: Install Tox - run: pip install tox + run: pip install tox wheel - name: Run Tox run: tox -e py36 @@ -66,7 +73,7 @@ jobs: python-version: 3.7 - name: Install Tox - run: pip install tox + run: pip install tox wheel - name: Run Tox run: tox -e py37 @@ -81,8 +88,9 @@ jobs: py38: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [macos-latest, ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v1 @@ -93,7 +101,7 @@ jobs: python-version: 3.8 - name: Install Tox - run: pip install tox + run: pip install tox wheel - name: Run Tox run: tox -e py38 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index afea882..9a43e8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: hooks: - id: seed-isort-config - repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 + rev: v5.4.2 hooks: - id: isort diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c12ff..6bcd8f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,25 @@ This project was forked from `py-solc`. View the original changelog [here](https ## [Unreleased](https://github.com/iamdefinitelyahuman/py-solc-x) +## [1.0.0](https://github.com/iamdefinitelyahuman/py-solc-x/releases/tag/v1.0.0) - 2020-08-26 +### Added +- "latest" is a valid version number when installing ([#104](https://github.com/iamdefinitelyahuman/py-solc-x/pull/104)) +- Custom exception classes ([#103](https://github.com/iamdefinitelyahuman/py-solc-x/pull/103)) +- Main compiler functions have `solc_version` and `solc_binary` kwargs for setting the version or using a custom solc binary ([#98](https://github.com/iamdefinitelyahuman/py-solc-x/pull/98)) +- MyPy types ([#99](https://github.com/iamdefinitelyahuman/py-solc-x/pull/99)) + +### Changed +- Major refactor of the main compiler functions and low-level solc wrapper ([#96](https://github.com/iamdefinitelyahuman/py-solc-x/pull/96)) +- Binaries are installed from [solc-bin.ethereum.org](https://solc-bin.ethereum.org/) instead of Github ([#108](https://github.com/iamdefinitelyahuman/py-solc-x/pull/108)) +- Building from source is now handled by a separate function `solcx.install.compile_solc` ([#108](https://github.com/iamdefinitelyahuman/py-solc-x/pull/108)) +- `get_available_solc_versions` has been split into `get_installable_solc_versions` and `get_compilable_solc_versions` ([#108](https://github.com/iamdefinitelyahuman/py-solc-x/pull/108)) +- `get_solc_folder` is now `get_solcx_install_folder` ([#102](https://github.com/iamdefinitelyahuman/py-solc-x/pull/102)) +- Paths are represented as `Path` objects instead of strings ([#97](https://github.com/iamdefinitelyahuman/py-solc-x/pull/97)) +- Solc versions are represented as `semantic_version.Version` objects instead of strings ([#93](https://github.com/iamdefinitelyahuman/py-solc-x/pull/93)) + +### Removed +- `utils.string` and `utils.types` subpackages ([#95](https://github.com/iamdefinitelyahuman/py-solc-x/pull/95)) + ## [0.10.1](https://github.com/iamdefinitelyahuman/py-solc-x/releases/tag/v0.10.1) - 2020-07-17 ### Fixed - Support ARM 64 bit architecture (`aarch64`) diff --git a/README.md b/README.md index e5b332e..bd669b0 100644 --- a/README.md +++ b/README.md @@ -2,189 +2,44 @@ [![Pypi Status](https://img.shields.io/pypi/v/py-solc-x.svg)](https://pypi.org/project/py-solc-x/) [![Build Status](https://img.shields.io/github/workflow/status/iamdefinitelyahuman/py-solc-x/py-solc-x%20workflow)](https://github.com/iamdefinitelyahuman/py-solc-x/actions) [![Coverage Status](https://img.shields.io/codecov/c/github/iamdefinitelyahuman/py-solc-x)](https://codecov.io/gh/iamdefinitelyahuman/py-solc-x) -Python wrapper around the `solc` Solidity compiler with `0.5.x` and `0.6.x` support. +Python wrapper and version management tool for the `solc` Solidity compiler. Forked from [`py-solc`](https://github.com/ethereum/py-solc). -## Dependencies - -Py-solc-x allows the use of multiple versions of solc and installs them as needed. You must have all required [solc dependencies](https://solidity.readthedocs.io/en/latest/installing-solidity.html#building-from-source) installed for it to work properly. +## Features -## Supported Versions +* Full support for Solidity `>=0.4.11` +* Install Solidity on Linux, OSX and Windows +* Compile Solidity from source on Linux and OSX -Py-solc-x can install the following solc versions: +## Dependencies -* Linux and Windows: `>=0.4.11` -* OSX: `>=0.5.0` +Py-solc-x allows the use of multiple versions of solc, and can install or compile them as needed. If you wish to compile from source you must first insall the required [solc dependencies](https://solidity.readthedocs.io/en/latest/installing-solidity.html#building-from-source). -See [Installing Solidity on OSX](https://github.com/iamdefinitelyahuman/py-solc-x/wiki/Installing-Solidity-on-OSX) for information on how to use `0.4.x` versions with OSX. -## Quickstart +## Installation -Installation +### via `pip` -```sh +```bash pip install py-solc-x ``` -## Installing the `solc` Executable - -The first time py-solc-x is imported it will automatically check for an installed version of solc on your system. If none is found, you must manually install via `solcx.install_solc`: - -```python ->>> from solcx import install_solc ->>> install_solc('v0.4.25') -``` - -Or via the command line: +### via `setuptools` ```bash -python -m solcx.install v0.4.25 -``` - -By default, `solc` versions are installed at `~/.solcx/`. If you wish to use a different directory you can specify it with the `SOLCX_BINARY_PATH` environment variable. - -## Setting the `solc` Version - -Py-solc-x defaults to the most recent installed version set as the active one. To check or modify the active version: - -```python ->>> from solcx import get_solc_version, set_solc_version ->>> get_solc_version() -Version('0.5.7+commit.6da8b019.Linux.gpp') ->>> set_solc_version('v0.4.25') ->>> -``` - -You can also set the version based on the pragma version string. The highest compatible version will be used: - -```python ->>> from solcx import set_solc_version_pragma ->>> set_solc_version_pragma('^0.4.20 || >0.5.5 <0.7.0') -Using solc version 0.5.8 ->>> set_solc_version_pragma('^0.4.20 || >0.5.5 <0.7.0', check_new=True) -Using solc version 0.5.8 -Newer compatible solc version exists: 0.6.0 +git clone https://github.com/iamdefinitelyahuman/py-solc-x.git +cd py-solc-x +python3 setup.py install ``` -To view available and installed versions: +## Documentation -```python ->>> from solcx import get_installed_solc_versions, get_available_solc_versions ->>> get_installed_solc_versions() -['v0.4.25', 'v0.5.3', 'v0.6.0'] ->>> get_available_solc_versions() -['v0.6.0', 'v0.5.15', 'v0.5.14', 'v0.5.13', 'v0.5.12', 'v0.5.11', 'v0.5.10', 'v0.5.9', 'v0.5.8', 'v0.5.7', 'v0.5.6', 'v0.5.5', 'v0.5.4', 'v0.5.3', 'v0.5.2', 'v0.5.1', 'v0.5.0', 'v0.4.25', 'v0.4.24', 'v0.4.23', 'v0.4.22', 'v0.4.21', 'v0.4.20', 'v0.4.19', 'v0.4.18', 'v0.4.17', 'v0.4.16', 'v0.4.15', 'v0.4.14', 'v0.4.13', 'v0.4.12', 'v0.4.11'] -``` +Documentation is hosted at [Read the Docs](https://solcx.readthedocs.io/en/latest/). -To install the highest compatible version based on the pragma version string: +## Testing -```python ->>> from solcx import install_solc_pragma ->>> install_solc_pragma('^0.4.20 || >0.5.5 <0.7.0') -``` - -## Standard JSON Compilation - -Use the `solcx.compile_standard` function to make use of the [standard-json](http://solidity.readthedocs.io/en/latest/using-the-compiler.html#compiler-input-and-output-json-description) compilation feature. - -```python ->>> from solcx import compile_standard ->>> compile_standard({ -... 'language': 'Solidity', -... 'sources': {'Foo.sol': 'content': "...."}, -... }) -{ - 'contracts': {...}, - 'sources': {...}, - 'errors': {...}, -} ->>> compile_standard({ -... 'language': 'Solidity', -... 'sources': {'Foo.sol': {'urls': ["/path/to/my/sources/Foo.sol"]}}, -... }, allow_paths="/path/to/my/sources") -{ - 'contracts': {...}, - 'sources': {...}, - 'errors': {...}, -} -``` - -## Legacy Combined JSON compilation - -```python ->>> from solcx import compile_source, compile_files ->>> compile_source("contract Foo { function Foo() {} }") -{ - 'Foo': { - 'abi': [{'inputs': [], 'type': 'constructor'}], - 'code': '0x60606040525b5b600a8060126000396000f360606040526008565b00', - 'code_runtime': '0x60606040526008565b00', - 'source': None, - 'meta': { - 'compilerVersion': '0.3.5-9da08ac3', - 'language': 'Solidity', - 'languageVersion': '0', - }, - }, -} ->>> compile_files(["/path/to/Foo.sol", "/path/to/Bar.sol"]) -{ - 'Foo': { - 'abi': [{'inputs': [], 'type': 'constructor'}], - 'code': '0x60606040525b5b600a8060126000396000f360606040526008565b00', - 'code_runtime': '0x60606040526008565b00', - 'source': None, - 'meta': { - 'compilerVersion': '0.3.5-9da08ac3', - 'language': 'Solidity', - 'languageVersion': '0', - }, - }, - 'Bar': { - 'abi': [{'inputs': [], 'type': 'constructor'}], - 'code': '0x60606040525b5b600a8060126000396000f360606040526008565b00', - 'code_runtime': '0x60606040526008565b00', - 'source': None, - 'meta': { - 'compilerVersion': '0.3.5-9da08ac3', - 'language': 'Solidity', - 'languageVersion': '0', - }, - }, -} -``` - -## Unlinked Libraries - -```python ->>> from solcx import link_code ->>> unlinked_bytecode = "606060405260768060106000396000f3606060405260e060020a6000350463e7f09e058114601a575b005b60187f0c55699c00000000000000000000000000000000000000000000000000000000606090815273__TestA_________________________________90630c55699c906064906000906004818660325a03f41560025750505056" ->>> link_code(unlinked_bytecode, {'TestA': '0xd3cda913deb6f67967b99d67acdfa1712c293601'}) -... "606060405260768060106000396000f3606060405260e060020a6000350463e7f09e058114601a575b005b60187f0c55699c00000000000000000000000000000000000000000000000000000000606090815273d3cda913deb6f67967b99d67acdfa1712c29360190630c55699c906064906000906004818660325a03f41560025750505056" -``` - -## Import Path Remappings - -`solc` provides path aliasing allow you to have more reusable project configurations. - -You can use this like: - -```python ->>> from solcx import compile_files - ->>> compile_files([source_file_path], import_remappings=["zeppeling=/my-zeppelin-checkout-folder"]) -``` - -[More information about solc import aliasing](http://solidity.readthedocs.io/en/latest/layout-of-source-files.html#paths) - -## Development - -This project was forked from [`py-solc`](https://github.com/ethereum/py-solc) and should be considered a beta. Comments, questions, criticisms and pull requests are welcomed. - -### Tests - -Py-solc-x is tested on Linux and Windows with solc versions ``>=0.4.11``. +Py-solc-x is tested on Linux, OSX and Windows with solc versions ``>=0.4.11``. To run the test suite: @@ -194,6 +49,12 @@ pytest tests/ By default, the test suite installs all available `solc` versions for your OS. If you only wish to test against already installed versions, include the `--no-install` flag. +## Contributing + +Help is always appreciated! Feel free to open an issue if you find a problem, or a pull request if you've solved an issue. + +Please check out our [Contribution Guide](CONTRIBUTING.md) prior to opening a pull request, and join the Brownie [Gitter channel](https://gitter.im/eth-brownie/community) if you have any questions. + ## License This project is licensed under the [MIT license](LICENSE). diff --git a/docs/_static/css/dark.css b/docs/_static/css/dark.css new file mode 100644 index 0000000..cb96b42 --- /dev/null +++ b/docs/_static/css/dark.css @@ -0,0 +1,207 @@ +/* links */ + +a, +a:visited { + color: #aaddff; +} + + +/* code directives */ + +.method dt, +.class dt, +.data dt, +.attribute dt, +.function dt, +.classmethod dt, +.exception dt, +.descclassname, +.descname { + background-color: #2d2d2d !important; +} + +.rst-content dl:not(.docutils) dt { + color: #aaddff; + border-top: solid 3px #525252; + border-left: solid 3px #525252; +} + +em.property { + color: #888888; +} + + +/* tables */ + +.rst-content table.docutils thead { + color: #ddd; +} + +.rst-content table.docutils td { + border: 0px; +} + +.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background-color: #5a5a5a; +} + + +/* inlined code highlights */ + +.xref, +.py-meth, +.rst-content a code { + color: #aaddff !important; + font-weight: normal !important; +} + +.rst-content code { + color: #eee !important; + font-weight: normal !important; +} + +code.literal { + background-color: #2d2d2d !important; + border: 1px solid #6d6d6d !important; +} + +code.docutils.literal.notranslate { + color: #ddd; +} + + +/* code examples */ + +pre { + background: #222; + color: #ddd; + font-size: 150%; + border-color: #333 !important; +} + +.copybutton { + color: #666 !important; + border-color: #333 !important; +} + +.highlight .go, +.highlight .nb, +.highlight .kn { + /* text */ + color: #ddd; + font-weight: normal; +} + +.highlight .o, +.highlight .p { + /* comparators, parentheses */ + color: #bbb; +} + +.highlight .c1 { + /* comments */ + color: #888; +} + +.highlight .bp { + /* self */ + color: #fc3; +} + +.highlight .mf, +.highlight .mi, +.highlight .kc { + /* numbers, booleans */ + color: #c90; +} + +.highlight .gt, +.highlight .nf, +.highlight .fm { + /* functions */ + color: #7cf; +} + +.highlight .nd { + /* decorators */ + color: #f66; +} + +.highlight .k, +.highlight .ow { + /* statements */ + color: #A7F; + font-weight: normal; +} + +.highlight .s2, +.highlight .s1, +.highlight .nt { + /* strings */ + color: #5d6; +} + + +/* notes, warnings, hints */ + +.hint .admonition-title { + background: #2aa87c !important; +} + +.warning .admonition-title { + background: #cc4444 !important; +} + +.admonition-title { + background: #3a7ca8 !important; +} + +.admonition, +.note { + background-color: #2d2d2d !important; +} + + +/* table of contents */ + +.wy-nav-content-wrap { + background-color: rgba(0, 0, 0, 0.6) !important; +} + +.sidebar { + background-color: #191919 !important; +} + +.sidebar-title { + background-color: #2b2b2b !important; +} + +.wy-menu-vertical a { + color: #ddd; +} + +.wy-menu-vertical code.docutils.literal.notranslate { + color: #404040; + background: none !important; + border: none !important; +} + +.wy-nav-content { + background: #3c3c3c; + color: #dddddd; +} + +.wy-menu-vertical li.on a, +.wy-menu-vertical li.current>a { + background: #a3a3a3; + border-bottom: 0px !important; + border-top: 0px !important; +} + +.wy-menu-vertical li.current { + background: #b3b3b3; +} + +.toc-backref { + color: grey !important; +} \ No newline at end of file diff --git a/docs/_static/css/toggle.css b/docs/_static/css/toggle.css new file mode 100644 index 0000000..ebbd065 --- /dev/null +++ b/docs/_static/css/toggle.css @@ -0,0 +1,77 @@ +input[type=checkbox] { + visibility: hidden; + height: 0; + width: 0; + margin: 0; +} + +.rst-versions .rst-current-version { + padding: 10px; + display: flex; + justify-content: space-between; +} + +.rst-versions .rst-current-version .fa-book, +.rst-versions .rst-current-version .fa-v, +.rst-versions .rst-current-version .fa-caret-down { + height: 24px; + line-height: 24px; + vertical-align: middle; +} + +.rst-versions .rst-current-version .fa-element { + width: 80px; + text-align: center; +} + +.rst-versions .rst-current-version .fa-book { + text-align: left; +} + +.rst-versions .rst-current-version .fa-v { + color: #27AE60; + text-align: right; +} + +label { + margin: 0 auto; + display: inline-block; + justify-content: center; + align-items: right; + border-radius: 100px; + position: relative; + cursor: pointer; + text-indent: -9999px; + width: 50px; + height: 21px; + background: #000; +} + +label:after { + border-radius: 50%; + position: absolute; + content: ''; + background: #fff; + width: 15px; + height: 15px; + top: 3px; + left: 3px; + transition: ease-in-out 200ms; +} + +input:checked+label { + background: #3a7ca8; +} + +input:checked+label:after { + left: calc(100% - 5px); + transform: translateX(-100%); +} + +html.transition, +html.transition *, +html.transition *:before, +html.transition *:after { + transition: ease-in-out 200ms !important; + transition-delay: 0 !important; +} \ No newline at end of file diff --git a/docs/_static/js/toggle.js b/docs/_static/js/toggle.js new file mode 100644 index 0000000..df13104 --- /dev/null +++ b/docs/_static/js/toggle.js @@ -0,0 +1,26 @@ +document.addEventListener('DOMContentLoaded', function() { + + var checkbox = document.querySelector('input[name=mode]'); + + function toggleCssMode(isDay) { + var mode = (isDay ? "Day" : "Night"); + localStorage.setItem("css-mode", mode); + + var darksheet = $('link[href="_static/css/dark.css"]')[0].sheet; + darksheet.disabled = isDay; + } + + if (localStorage.getItem("css-mode") == "Day") { + toggleCssMode(true); + checkbox.setAttribute('checked', true); + } + + checkbox.addEventListener('change', function() { + document.documentElement.classList.add('transition'); + window.setTimeout(() => { + document.documentElement.classList.remove('transition'); + }, 1000) + toggleCssMode(this.checked); + }) + +}); \ No newline at end of file diff --git a/docs/_templates/versions.html b/docs/_templates/versions.html new file mode 100644 index 0000000..f680b65 --- /dev/null +++ b/docs/_templates/versions.html @@ -0,0 +1,36 @@ +{# Add rst-badge after rst-versions for small badge style. #} +
+ + RTD + + + + + + + v: {{ current_version }} + + +
+
+
{{ _('Versions') }}
{% for slug, url in versions %} +
{{ slug }}
+ {% endfor %} +
+
+
{{ _('Downloads') }}
{% for type, url in downloads %} +
{{ type }}
+ {% endfor %} +
+
+ {# Translators: The phrase "Read the Docs" is not translated #} +
{{ _('On Read the Docs') }}
+
+ {{ _('Project Home') }} +
+
+ {{ _('Builds') }} +
+
+
+
\ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..0856139 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,195 @@ +#!/usr/bin/python3 + +from pathlib import Path +from typing import Dict, List + +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +def setup(sphinx): + pass + + +with Path(__file__).parent.joinpath("../setup.py").open() as fp: + for line in fp: + if "version=" in line: + setup_version = line.split('"')[1] + break + + +# -- Project information ----------------------------------------------------- + + +project = "py-solc-x" +copyright = "2020" +author = "Ben Hauser" + +# The short X.Y version +version = setup_version +# The full version, including alpha/beta/rc tags +release = setup_version + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions: List = ["sphinx.ext.intersphinx"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "toctree" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +html_css_files = ["css/toggle.css", "css/dark.css"] + +html_js_files = ["js/toggle.js"] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "Browniedoc" + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements: Dict = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [(master_doc, "solcx.tex", "py-solc-x Documentation", "Ben Hauser", "manual")] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "solcx", "py-solc-x Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "py-solc-x", + "py-solc-x Documentation", + author, + "py-solc-x", + "Python wrapper and version management tool for the solc Solidity compiler.", + "py-solc-x", + ) +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ["search.html"] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3.8/", None), + "semantic_version": ("https://python-semanticversion.readthedocs.io/en/latest/", None), +} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..fbb8e22 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +========= +py-solc-x +========= + +Python wrapper and version management tool for the ``solc`` `Solidity `_ compiler. + +Features +======== + +* Full support for Solidity versions ``>=0.4.11`` +* Installs Solidity on Linux, OSX and Windows +* Compiles Solidity from source on Linux and OSX + +Credit +====== + +`py-solc-x `_ is forked from `py-solc `_ which was written by `Piper Merriam `_. + +Dependencies +============ + +Py-solc-x allows the use of multiple versions of solc, and can install or compile them as needed. If you wish to compile from source you must first install the required `solc dependencies `_. diff --git a/docs/solc-wrapper.rst b/docs/solc-wrapper.rst new file mode 100644 index 0000000..10bcf05 --- /dev/null +++ b/docs/solc-wrapper.rst @@ -0,0 +1,34 @@ +============================= +The Low-Level Process Wrapper +============================= + +Along with the :ref:`main compiler functions `, you can also directly call ``solc`` using the low-level wrapper. + +.. py:function:: solc_wrapper(solc_binary=None, stdin=None, source_files=None, import_remappings=None, success_return_code=None, **kwargs) + + Wrapper function for calling to ``solc``. + + Returns the process ``stdout`` as a string, ``stderr`` as a string, the full command executed as a list of strings, and the completed :py:class:`Popen ` object used to call ``solc``. + + **Arguments** + + ``solc_binary`` : Path | str + Location of the ``solc`` binary. If not given, the current default binary is used. + ``stdin`` : str + Input to pass to ``solc`` via stdin + ``source_files`` List | Path | str + Solidity source file, or list of source files, to be compiled. Files may be given as strings or :py:class:`Path ` objects. + ``import_remappings`` : Dict | List | str + Path remappings. May be given as a string or list of strings formatted as ``"prefix=path"`` + or a dict of ``{"prefix": "path"}`` + ``success_return_code`` : int + Expected exit code. Raises ``SolcError`` if the process returns a different value. Defaults to ``0``. + ``**kwargs`` Any + Flags to be passed to `solc`. Keywords are converted to flags by prepending ``--`` and replacing ``_`` with ``-``, for example the keyword ``evm_version`` becomes ``--evm-version``. Values may be given in the following formats: + + * ``False`` or ``None``: The flag is ignored + * ``True``: The flag is passed to the compiler without any arguments + * ``str``: The value is given as an argument without any modification + * ``int``: The value is converted to a string + * ``Path``: The value is converted to a string via :py:meth:`Path.as_posix ` + * ``List`` or ``Tuple``: Elements in the sequence are converted to strings and joined with ``,`` diff --git a/docs/toctree.rst b/docs/toctree.rst new file mode 100644 index 0000000..ae76270 --- /dev/null +++ b/docs/toctree.rst @@ -0,0 +1,11 @@ +======= +Brownie +======= + +.. toctree:: + :maxdepth: 2 + + Overview + using-the-compiler.rst + solc-wrapper.rst + version-management.rst diff --git a/docs/using-the-compiler.rst b/docs/using-the-compiler.rst new file mode 100644 index 0000000..4d212e6 --- /dev/null +++ b/docs/using-the-compiler.rst @@ -0,0 +1,245 @@ +.. _using-the-compiler: + +================== +Using the Compiler +================== + +py-solc-x provides several functions that you can use to interact with the ``solc`` compiler. + +Compiling a Source String +========================= + +.. py:function:: solcx.compile_source(source, **kwargs) + + Compile a Solidity contract. + + Compilation is handled via the ``--combined-json`` flag. Depending on the Solidity version used, some keyword arguments may not be available. + + Returns a dict, where each top-level key is a contract. The filename will be ````. + + .. code-block:: python + + >>> import solcx + >>> solcx.compile_source( + ... "contract Foo { function bar() public { return; } }", + ... output_values=["abi", "bin-runtime"], + ... solc_version="0.7.0" + ... ) + { + ':Foo': { + 'abi': [{'inputs': [], 'name': 'bar', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}], + 'bin-runtime': '6080604052348015600f57600080fd5b506004361060285760003560e01c8063febb0f7e14602d575b600080fd5b60336035565b005b56fea26469706673582212203cfdbce82ee8eab351107edac2ebb9dbe5c1aa8bd26609b0eedaa105ed3d4dce64736f6c63430007000033' + } + } + + **Required Arguments** + + ``source`` str + Solidity contract to be compiled. + + **Optional py-solc-x Arguments** + + ``solc_binary`` str | Path + Path of the ``solc`` binary to use. May be given as a string or :py:class:`Path ` object. If not given, the currently active version is used (as set by :func:`solcx.set_solc_version `) + ``solc_version`` str | Version + ``solc`` version to use. May be given as a string or :py:class:`Version ` object. If not given, the currently active version is used. Ignored if ``solc_binary`` is also given. + ``allow_empty`` bool + If ``True``, do not raise when no compiled contracts are returned. Defaults to ``False``. + + **Optional Compiler Arguments** + + Depending on the Solidity version used, using some of these arguments may raise ``UnknownOption``. See the documentation for your target Solidity version for more information. + + ``output_values`` List + Compiler outputs to return. Valid options depend on the version of ``solc``. + If not given, all possible outputs for the active version are returned. + ``import_remappings`` Dict | List | str + Path remappings. May be given as a string or list of strings formatted as + ``"prefix=path"``, or a dict of ``{"prefix": "path"}``. + ``base_path`` Path | str + Use the given path as the root of the source tree instead of the root + of the filesystem. + ``allow_paths`` List | Path | str + A path, or list of paths, to allow for imports. + ``output_dir`` str + Creates one file per component and contract/file at the specified directory. + ``overwrite`` bool + Overwrite existing files (used in combination with ``output_dir``) + ``evm_version`` str + Select the desired EVM version. Valid options depend on the ``solc`` version. + ``revert_strings`` List | str + Strip revert (and require) reason strings or add additional debugging + information. + ``metadata_hash`` str + Choose hash method for the bytecode metadata or disable it. + ``metadata_literal`` bool + Store referenced sources as literal data in the metadata output. + ``optimize`` bool + Enable bytecode optimizer. + ``optimize_runs`` int + Set for how many contract runs to optimize. Lower values will optimize + more for initial deployment cost, higher values will optimize more for + high-frequency usage. + ``optimize_yul`` bool + Enable the yul optimizer. + ``no_optimize_yul`` bool + Disable the yul optimizer. + ``yul_optimizations`` int + Force yul optimizer to use the specified sequence of optimization steps + instead of the built-in one. + +Compiling Files +=============== + +.. py:function:: solcx.compile_files(source, **kwargs) + + Compile one or more Solidity source files. + + Compilation is handled via the ``--combined-json`` flag. Depending on the Solidity version used, some keyword arguments may not be available. + + Returns a dict, where each top-level key is a contract. + + .. code-block:: python + + >>> import solcx + >>> solcx.compile_files( + ... ["Foo.sol"], + ... output_values=["abi", "bin-runtime"], + ... solc_version="0.7.0" + ... ) + { + ':Foo': { + 'abi': [{'inputs': [], 'name': 'bar', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}], + 'bin-runtime': '6080604052348015600f57600080fd5b506004361060285760003560e01c8063febb0f7e14602d575b600080fd5b60336035565b005b56fea26469706673582212203cfdbce82ee8eab351107edac2ebb9dbe5c1aa8bd26609b0eedaa105ed3d4dce64736f6c63430007000033' + } + } + + **Required Arguments** + + ``source_files`` List | Path | str + Solidity source file, or list of source files, to be compiled. Files may be given as strings or :py:class:`Path ` objects. + + **Optional py-solc-x Arguments** + + ``solc_binary`` str | Path + Path of the ``solc`` binary to use. May be given as a string or :py:class:`Path ` object. If not given, the currently active version is used (as set by :func:`solcx.set_solc_version `) + ``solc_version`` str | Version + ``solc`` version to use. May be given as a string or :py:class:`Version ` object. If not given, the currently active version is used. Ignored if ``solc_binary`` is also given. + ``allow_empty`` bool + If ``True``, do not raise when no compiled contracts are returned. Defaults to ``False``. + + **Optional Compiler Arguments** + + Depending on the Solidity version used, using some of these arguments may raise ``UnknownOption``. See the documentation for your target Solidity version for more information. + + ``output_values`` List + Compiler outputs to return. Valid options depend on the version of ``solc``. + If not given, all possible outputs for the active version are returned. + ``import_remappings`` Dict | List | str + Path remappings. May be given as a string or list of strings formatted as + ``"prefix=path"``, or a dict of ``{"prefix": "path"}``. + ``base_path`` Path | str + Use the given path as the root of the source tree instead of the root + of the filesystem. + ``allow_paths`` List | Path | str + A path, or list of paths, to allow for imports. + ``output_dir`` str + Creates one file per component and contract/file at the specified directory. + ``overwrite`` bool + Overwrite existing files (used in combination with ``output_dir``) + ``evm_version`` str + Select the desired EVM version. Valid options depend on the ``solc`` version. + ``revert_strings`` List | str + Strip revert (and require) reason strings or add additional debugging + information. + ``metadata_hash`` str + Choose hash method for the bytecode metadata or disable it. + ``metadata_literal`` bool + Store referenced sources as literal data in the metadata output. + ``optimize`` bool + Enable bytecode optimizer. + ``optimize_runs`` int + Set for how many contract runs to optimize. Lower values will optimize + more for initial deployment cost, higher values will optimize more for + high-frequency usage. + ``optimize_yul`` bool + Enable the yul optimizer. + ``no_optimize_yul`` bool + Disable the yul optimizer. + ``yul_optimizations`` int + Force yul optimizer to use the specified sequence of optimization steps + instead of the built-in one. + +Compiling with the Standard JSON Format +======================================= + +.. py:function:: solcx.compile_standard(input_data, **kwargs) + + Compile Solidity contracts using the JSON-input-output interface. + + See the Solidity documentation on `the compiler input-output JSON `_ for details on the expected JSON input and output formats. + + **Required Arguments** + + ``input_data`` Dict + Compiler JSON input. + + **Optional py-solc-x Arguments** + + ``solc_binary`` str | Path + Path of the ``solc`` binary to use. May be given as a string or :py:class:`Path ` object. If not given, the currently active version is used (as set by :func:`solcx.set_solc_version `) + ``solc_version`` str | Version + ``solc`` version to use. May be given as a string or :py:class:`Version ` object. If not given, the currently active version is used. Ignored if ``solc_binary`` is also given. + ``allow_empty`` bool + If ``True``, do not raise when no compiled contracts are returned. Defaults to ``False``. + + **Optional Compiler Arguments** + + Depending on the Solidity version used, using some of these arguments may raise ``UnknownOption``. See the documentation for your target Solidity version for more information. + + ``base_path`` Path | str + Use the given path as the root of the source tree instead of the root + of the filesystem. + ``allow_paths`` List | Path | str + A path, or list of paths, to allow for imports. + ``output_dir`` str + Creates one file per component and contract/file at the specified directory. + ``overwrite`` bool + Overwrite existing files (used in combination with ``output_dir``) + +Linking Libraries +================= + +.. py:function:: solcx.link_code(unlinked_bytecode, libraries, solc_binary=None, solc_version=None) + + Add library addresses into unlinked bytecode. + + See the Solidity documentation on `using the commandline compiler `_ for more information on linking libraries. + + Returns the linked bytecode as a string. + + .. code-block:: python + + >>> import solcx + >>> unlinked_bytecode = "606060405260768060106000396000f3606060405260e060020a6000350463e7f09e058114601a575b005b60187f0c55699c00000000000000000000000000000000000000000000000000000000606090815273__TestA_________________________________90630c55699c906064906000906004818660325a03f41560025750505056" + + >>> solcx.link_code( + ... unlinked_bytecode, + ... {'TestA': "0xd3cda913deb6f67967b99d67acdfa1712c293601"} + ... ) + "606060405260768060106000396000f3606060405260e060020a6000350463e7f09e058114601a575b005b60187f0c55699c00000000000000000000000000000000000000000000000000000000606090815273d3cda913deb6f67967b99d67acdfa1712c29360190630c55699c906064906000906004818660325a03f41560025750505056" + + + **Required Arguments** + + ``unlinked_bytecode`` str + Compiled bytecode containing one or more library placeholders. + ``libraries`` Dict + Library addresses given as ``{"library name": "address"}`` + + **Optional py-solc-x Arguments** + + ``solc_binary`` str | Path + Path of the ``solc`` binary to use. May be given as a string or :py:class:`Path ` object. If not given, the currently active version is used (as set by :func:`solcx.set_solc_version `) + ``solc_version`` str | Version + ``solc`` version to use. May be given as a string or :py:class:`Version ` object. If not given, the currently active version is used. Ignored if ``solc_binary`` is also given. diff --git a/docs/version-management.rst b/docs/version-management.rst new file mode 100644 index 0000000..2b68ce2 --- /dev/null +++ b/docs/version-management.rst @@ -0,0 +1,177 @@ +=========================== +Solidity Version Management +=========================== + +Installation Folder +=================== + +By default, ``solc`` versions are installed at ``~/.solcx/``. Each installed version is named using the following pattern: ``solc-v[MAJOR].[MINOR].[PATH]`` + +If you wish to install to a different directory you can specify it with the ``SOLCX_BINARY_PATH`` environment variable. You can also give a custom directory to most installation functions using the optional ``solcx_binary_path`` keyword argument. + +.. py:function:: solcx.get_solcx_install_folder(solcx_binary_path=None) + + Return the directory where py-solc-x stores installed ``solc`` binaries. + + .. code-block:: python + + >>> solcx.get_solcx_install_folder() + PosixPath('/home/computer/.solcx') + +Getting and Setting the Active Version +====================================== + +When py-solc-x is imported, it attempts to locate an installed version of ``solc`` using ``which`` on Linux or OSX systems, or ``where.exe`` on Windows. If found, this version is set as the active version. If not found, it uses the latest version that has been installed by py-solc-x. + + +Getting the Active Version +-------------------------- + +Use the following methods to check the active ``solc`` version: + +.. py:function:: solcx.get_solc_version() + + Return the version of the current active ``solc`` binary, as a :py:class:`Version ` object. + + .. code-block:: python + + >>> solcx.get_solc_version() + Version('0.7.0') + +.. py:function:: solcx.install.get_executable(version=None, solcx_binary_path=None) + + Return a :py:class:`Path ` object for a ``solc`` binary. + + If no arguments are given, returns the current active version. If a version is specified, returns the installed binary matching the given version. + + Raises ``SolcNotInstalled`` if no binary is found. + + .. code-block:: python + + >>> solcx.install.get_executable() + PosixPath('/usr/bin/solc') + +.. py:function:: solcx.get_installed_solc_versions(solcx_binary_path=None) + + Return a list of currently installed ``solc`` versions. + + .. code-block:: python + + >>> solcx.get_installed_solc_versions() + [Version('0.7.0'), Version('0.6.8'), Version('0.6.3'), Version('0.5.7'), Version('0.4.25')] + +Setting the Active Version +-------------------------- + +.. py:function:: solcx.set_solc_version(version, silent=False, solcx_binary_path=None) + + Set the currently active ``solc`` version. + + .. code-block:: python + + >>> solcx.set_solc_version('0.5.0') + +.. py:function:: solcx.set_solc_version_pragma(pragma_string, silent=False, check_new=False) + + Set the currently active ``solc`` binary based on a pragma statement. + + The newest installed version that matches the pragma is chosen. Raises ``SolcNotInstalled`` if no installed versions match. + + .. code-block:: python + + >>> solcx.set_solc_version_pragma('pragma solidity ^0.5.0;') + Version('0.5.17') + + +Importing Already-Installed Versions +==================================== + +.. py:function:: solcx.import_installed_solc(solcx_binary_path=None) + + Search for and copy installed ``solc`` versions into the local installation folder. + + This function is especially useful on OSX, to access Solidity versions that you have installed from homebrew and where a precompiled binary is not available. + + .. code-block:: python + + >>> solcx.import_installed_solc() + [Version('0.7.0'), Version('0.6.12')] + + +Installing Solidity +=================== + +py-solc-x downloads and installs precompiled binaries from `solc-bin.ethereum.org `_. Different binaries are available depending on your operating system. + +Getting Installable Versions +---------------------------- + +.. py:function:: solcx.get_installable_solc_versions() + + Return a list of all ``solc`` versions that can be installed by py-solc-x. + + + .. code-block:: python + + >>> solcx.get_installable_solc_versions() + [Version('0.7.0'), Version('0.6.12'), Version('0.6.11'), Version('0.6.10'), Version('0.6.9'), Version('0.6.8'), Version('0.6.7'), Version('0.6.6'), Version('0.6.5'), Version('0.6.4'), Version('0.6.3'), Version('0.6.2'), Version('0.6.1'), Version('0.6.0'), Version('0.5.17'), Version('0.5.16'), Version('0.5.15'), Version('0.5.14'), Version('0.5.13'), Version('0.5.12'), Version('0.5.11'), Version('0.5.10'), Version('0.5.9'), Version('0.5.8'), Version('0.5.7'), Version('0.5.6'), Version('0.5.5'), Version('0.5.4'), Version('0.5.3'), Version('0.5.2'), Version('0.5.1'), Version('0.5.0'), Version('0.4.26'), Version('0.4.25'), Version('0.4.24'), Version('0.4.23'), Version('0.4.22'), Version('0.4.21'), Version('0.4.20'), Version('0.4.19'), Version('0.4.18'), Version('0.4.17'), Version('0.4.16'), Version('0.4.15'), Version('0.4.14'), Version('0.4.13'), Version('0.4.12'), Version('0.4.11')] + +Installing Precompiled Binaries +------------------------------- + +.. py:function:: solcx.install_solc(version="latest", show_progress=False, solcx_binary_path=None) + + Download and install a precompiled ``solc`` binary. + + ``version`` str | Version + Version of ``solc`` to install. Default is the newest available version. + ``show_progress`` bool + If ``True``, display a progress bar while downloading. Requires installing + the `tqdm `_ package. + ``solcx_binary_path`` Path | str + User-defined path, used to override the default installation directory. + +Building from Source +==================== + +When a precompiled version of Solidity isn't available for your operating system, you may still install it by building from the source code. Source code is downloaded from `Github `_. + +.. note:: + + If you wish to compile from source you must first install the required `solc dependencies `_. + + +Getting Compilable Versions +--------------------------- + +.. py:function:: solcx.get_compilable_solc_versions(headers=None) + + Return a list of all ``solc`` versions that can be installed by py-solc-x. + + ``headers`` Dict + Headers to include in the request to Github. + + .. code-block:: python + + >>> solcx.get_compilable_solc_versions() + [Version('0.7.0'), Version('0.6.12'), Version('0.6.11'), Version('0.6.10'), Version('0.6.9'), Version('0.6.8'), Version('0.6.7'), Version('0.6.6'), Version('0.6.5'), Version('0.6.4'), Version('0.6.3'), Version('0.6.2'), Version('0.6.1'), Version('0.6.0'), Version('0.5.17'), Version('0.5.16'), Version('0.5.15'), Version('0.5.14'), Version('0.5.13'), Version('0.5.12'), Version('0.5.11'), Version('0.5.10'), Version('0.5.9'), Version('0.5.8'), Version('0.5.7'), Version('0.5.6'), Version('0.5.5'), Version('0.5.4'), Version('0.5.3'), Version('0.5.2'), Version('0.5.1'), Version('0.5.0'), Version('0.4.26'), Version('0.4.25'), Version('0.4.24'), Version('0.4.23'), Version('0.4.22'), Version('0.4.21'), Version('0.4.20'), Version('0.4.19'), Version('0.4.18'), Version('0.4.17'), Version('0.4.16'), Version('0.4.15'), Version('0.4.14'), Version('0.4.13'), Version('0.4.12'), Version('0.4.11')] + + +Compiling Solidity from Source +------------------------------ + +.. py:function:: solcx.compile_solc(version, show_progress=False, solcx_binary_path=None) + + Install a version of ``solc`` by downloading and compiling source code. + + This function is only available when using Linux or OSX. + + **Arguments:** + + ``version`` str | Version + Version of ``solc`` to install. + ``show_progress`` bool + If ``True``, display a progress bar while downloading. Requires installing + the `tqdm `_ package. + ``solcx_binary_path`` Path | str + User-defined path, used to override the default installation directory. diff --git a/requirements-dev.txt b/requirements-dev.txt index cc26b40..7e3ce1d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,11 +1,15 @@ black==19.10b0 bumpversion==0.5.3 -flake8==3.7.9 -isort==4.3.21 -pytest==5.4.1 -pytest-cov==2.8.1 +flake8==3.8.3 +isort==5.4.2 +mypy==0.782 +pytest>=6.0.0,<7.0.0 +pytest-cov==2.10.1 semantic_version>=2.8.1,<3 -tox==3.14.6 -tqdm>=4.41.0,<5.0.0 -twine==1.13.0 +sphinx==3.2.1 +sphinx_rtd_theme==0.5.0 +tox==3.19.0 +tqdm>=4.48.0,<5.0.0 +twine==3.2.0 requests>=2.19.0,<3 +wheel==0.35.1 diff --git a/setup.cfg b/setup.cfg index ee2afc8..d7514d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,17 @@ [bumpversion] -current_version = 0.10.1 +current_version = 1.0.0 [bumpversion:file:setup.py] [flake8] max-line-length = 100 ignore = E203,W503 +per-file-ignores = + */__init__.py: F401 + +[mypy] +ignore_missing_imports = True +follow_imports = silent [tool:isort] force_grid_wrap = 0 @@ -17,3 +23,4 @@ use_parentheses = True [tool:pytest] addopts = --cov=solcx --cov-branch --cov-report xml + diff --git a/setup.py b/setup.py index 514ad30..1f63f80 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="py-solc-x", - version="0.10.1", # don't change this manually, use bumpversion instead + version="1.0.0", # don't change this manually, use bumpversion instead description="Python wrapper around the solc binary with 0.5.x and 0.6.x support", long_description_markdown_filename="README.md", author="Ben Hauser (forked from py-solc by Piper Merriam)", @@ -13,11 +13,8 @@ include_package_data=True, py_modules=["solcx"], setup_requires=["setuptools-markdown"], - python_requires=">=3.4, <4", - install_requires=[ - "requests>=2.19.0,<3", - "semantic_version>=2.8.1,<3", - ], + python_requires=">=3.6, <4", + install_requires=["requests>=2.19.0,<3", "semantic_version>=2.8.1,<3"], license="MIT", zip_safe=False, keywords="ethereum solidity solc", @@ -27,8 +24,6 @@ "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/solcx/__init__.py b/solcx/__init__.py index f55f5f7..0e8284c 100644 --- a/solcx/__init__.py +++ b/solcx/__init__.py @@ -1,27 +1,13 @@ -from __future__ import absolute_import - -from .install import ( # noqa: F401 - get_available_solc_versions, +from solcx.install import ( + compile_solc, + get_compilable_solc_versions, + get_installable_solc_versions, get_installed_solc_versions, - get_solc_folder, + get_solcx_install_folder, import_installed_solc, install_solc, install_solc_pragma, set_solc_version, set_solc_version_pragma, ) -from .main import ( # noqa: F401 - compile_files, - compile_source, - compile_standard, - get_solc_version, - get_solc_version_string, - link_code, -) - -# check for installed version of solc -import_installed_solc() - -# default to latest version -if get_installed_solc_versions(): - set_solc_version(get_installed_solc_versions()[-1], silent=True) +from solcx.main import compile_files, compile_source, compile_standard, get_solc_version, link_code diff --git a/solcx/exceptions.py b/solcx/exceptions.py index b15e402..0c8d53b 100644 --- a/solcx/exceptions.py +++ b/solcx/exceptions.py @@ -1,43 +1,37 @@ -import textwrap - -from .utils.string import force_text - - -def force_text_maybe(value, encoding="iso-8859-1"): - if value is not None: - return force_text(value) - - -DEFAULT_MESSAGE = "An error occurred during execution" +from typing import Dict, List class SolcError(Exception): - message = DEFAULT_MESSAGE + message = "An error occurred during execution" - def __init__(self, command, return_code, stdin_data, stdout_data, stderr_data, message=None): + def __init__( + self, + message: str = None, + command: List = None, + return_code: int = None, + stdin_data: str = None, + stdout_data: str = None, + stderr_data: str = None, + error_dict: Dict = None, + ) -> None: if message is not None: self.message = message - self.command = command + self.command = command or [] self.return_code = return_code - self.stdin_data = force_text_maybe(stdin_data, "utf8") - self.stderr_data = force_text_maybe(stderr_data, "utf8") - self.stdout_data = force_text_maybe(stdout_data, "utf8") - - def __str__(self): - return textwrap.dedent( - ( - """ - {s.message} - > command: `{command}` - > return code: `{s.return_code}` - > stderr: - {s.stdout_data} - > stdout: - {s.stderr_data} - """ - ).format( - s=self, command=" ".join(self.command), - ) + self.stdin_data = stdin_data + self.stderr_data = stderr_data + self.stdout_data = stdout_data + self.error_dict = error_dict + + def __str__(self) -> str: + return ( + f"{self.message}" + f"\n> command: `{' '.join(str(i) for i in self.command)}`" + f"\n> return code: `{self.return_code}`" + "\n> stdout:" + f"\n{self.stdout_data}" + "\n> stderr:" + f"\n{self.stderr_data}" ).strip() @@ -45,9 +39,33 @@ class ContractsNotFound(SolcError): message = "No contracts found during compilation" +class SolcInstallationError(Exception): + pass + + +class UnknownOption(AttributeError): + pass + + +class UnknownValue(ValueError): + pass + + +class UnexpectedVersionError(Exception): + pass + + +class UnsupportedVersionError(ValueError): + pass + + class SolcNotInstalled(Exception): pass class DownloadError(Exception): pass + + +class UnexpectedVersionWarning(Warning): + pass diff --git a/solcx/install.py b/solcx/install.py index 4a4ee41..539c766 100644 --- a/solcx/install.py +++ b/solcx/install.py @@ -4,7 +4,6 @@ import argparse import logging import os -import platform import re import shutil import stat @@ -12,16 +11,26 @@ import sys import tarfile import tempfile +import warnings import zipfile from base64 import b64encode from io import BytesIO from pathlib import Path +from typing import Dict, List, Optional, Union import requests from semantic_version import SimpleSpec, Version -from .exceptions import DownloadError, SolcNotInstalled -from .utils.lock import get_process_lock +from solcx import wrapper +from solcx.exceptions import ( + DownloadError, + SolcInstallationError, + SolcNotInstalled, + UnexpectedVersionError, + UnexpectedVersionWarning, + UnsupportedVersionError, +) +from solcx.utils.lock import get_process_lock try: from tqdm import tqdm @@ -29,44 +38,62 @@ tqdm = None -DOWNLOAD_BASE = "https://github.com/ethereum/solidity/releases/download/{}/{}" -ALL_RELEASES = "https://api.github.com/repos/ethereum/solidity/releases?per_page=100" +BINARY_DOWNLOAD_BASE = "https://solc-bin.ethereum.org/{}-amd64/{}" +SOURCE_DOWNLOAD_BASE = "https://github.com/ethereum/solidity/releases/download/v{}/{}" +GITHUB_RELEASES = "https://api.github.com/repos/ethereum/solidity/releases?per_page=100" -MINIMAL_SOLC_VERSION = "v0.4.11" -VERSION_REGEX = { - "darwin": "solidity_[0-9].[0-9].[0-9]{1,}.tar.gz", - "linux": "solc-static-linux", - "win32": "solidity-windows.zip", -} +MINIMAL_SOLC_VERSION = Version("0.4.11") LOGGER = logging.getLogger("solcx") SOLCX_BINARY_PATH_VARIABLE = "SOLCX_BINARY_PATH" -solc_version = None +_default_solc_binary = None -def _get_arch(): - if platform.machine().startswith("arm") or platform.machine() == "aarch64": - return "arm" - if platform.machine().startswith("x86"): - return "x86" - else: - return platform.machine() - - -def _get_platform(): +def _get_os_name() -> str: if sys.platform.startswith("linux"): return "linux" - if sys.platform in ("darwin", "win32"): - return sys.platform - raise KeyError( - "Unknown platform: '{}' - py-solc-x supports" " Linux, OSX and Windows".format(sys.platform) - ) + if sys.platform == "darwin": + return "macosx" + if sys.platform == "win32": + return "windows" + raise OSError(f"Unsupported OS: '{sys.platform}' - py-solc-x supports Linux, OSX and Windows") + + +def _convert_and_validate_version(version: Union[str, Version]) -> Version: + # take a user-supplied version as a string or Version + # validate the value, and return a Version object + if not isinstance(version, Version): + version = Version(version.lstrip("v")) + if version not in SimpleSpec(">=0.4.11"): + raise UnsupportedVersionError("py-solc-x does not support solc versions <0.4.11") + return version + + +def _unlink_solc(solc_path: Path) -> None: + solc_path.unlink() + if _get_os_name() == "windows": + shutil.rmtree(solc_path.parent) + + +def get_solcx_install_folder(solcx_binary_path: Union[Path, str] = None) -> Path: + """ + Return the directory where `py-solc-x` stores installed `solc` binaries. + + By default, this is `~/.solcx` + Arguments + --------- + solcx_binary_path : Path | str, optional + User-defined path, used to override the default installation directory. -def get_solc_folder(solcx_binary_path=None): + Returns + ------- + Path + Subdirectory where `solc` binaries are are saved. + """ if os.getenv(SOLCX_BINARY_PATH_VARIABLE): - return Path(os.getenv(SOLCX_BINARY_PATH_VARIABLE)) + return Path(os.environ[SOLCX_BINARY_PATH_VARIABLE]) elif solcx_binary_path is not None: return Path(solcx_binary_path) else: @@ -75,118 +102,262 @@ def get_solc_folder(solcx_binary_path=None): return path -def _import_version(path): - version = subprocess.check_output([path, "--version"]).decode() - return "v" + version[version.index("Version: ") + 9 : version.index("+")] +def _get_which_solc() -> Path: + # get the path for the currently installed `solc` version, if any + if _get_os_name() == "windows": + response = subprocess.check_output(["where.exe", "solc"], encoding="utf8").strip() + else: + response = subprocess.check_output(["which", "solc"], encoding="utf8").strip() + + return Path(response) + +def import_installed_solc(solcx_binary_path: Union[Path, str] = None) -> List[Version]: + """ + Search for and copy installed `solc` versions into the local installation folder. -def import_installed_solc(solcx_binary_path=None): - platform = _get_platform() - if platform == "win32": - return + Arguments + --------- + solcx_binary_path : Path | str, optional + User-defined path, used to override the default installation directory. - # copy active version of solc - path_list = [] - which = ( - subprocess.run(["which", "solc"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - .stdout.decode() - .strip() - ) - if which: - path_list.append(which) + Returns + ------- + List + Imported solc versions + """ + try: + path_list = [_get_which_solc()] + except (FileNotFoundError, subprocess.CalledProcessError): + path_list = [] # on OSX, also copy all versions of solc from cellar - if platform == "darwin": - path_list = [str(i) for i in Path("/usr/local/Cellar").glob("solidity*/**/solc")] + if _get_os_name() == "macosx": + path_list.extend(Path("/usr/local/Cellar").glob("solidity*/**/solc")) + imported_versions = [] for path in path_list: try: - version = _import_version(path) + version = wrapper._get_solc_version(path) assert version not in get_installed_solc_versions() except Exception: continue - copy_path = str( - get_solc_folder(solcx_binary_path=solcx_binary_path).joinpath("solc-" + version) - ) + + copy_path = get_solcx_install_folder(solcx_binary_path).joinpath(f"solc-v{version}") + if _get_os_name() == "windows": + copy_path.mkdir() + copy_path = copy_path.joinpath("solc.exe") + shutil.copy(path, copy_path) try: # confirm that solc still works after being copied - assert version == _import_version(copy_path) + assert version == wrapper._get_solc_version(copy_path) + imported_versions.append(version) except Exception: - os.unlink(copy_path) - - -def get_executable(version=None, solcx_binary_path=None): + _unlink_solc(copy_path) + + return imported_versions + + +def get_executable( + version: Union[str, Version] = None, solcx_binary_path: Union[Path, str] = None +) -> Path: + """ + Return the Path to an installed `solc` binary. + + Arguments + --------- + version : str | Version, optional + Installed `solc` version to get the path of. If not given, returns the + path of the active version. + solcx_binary_path : Path | str, optional + User-defined path, used to override the default installation directory. + + Returns + ------- + Path + `solc` executable. + """ if not version: - version = solc_version - if not version: - raise SolcNotInstalled( - "Solc is not installed. Call solcx.get_available_solc_versions()" - " to view for available versions and solcx.install_solc() to install." - ) - solc_bin = get_solc_folder(solcx_binary_path=solcx_binary_path).joinpath("solc-" + version) - if sys.platform == "win32": + if not _default_solc_binary: + raise SolcNotInstalled( + "Solc is not installed. Call solcx.get_available_solc_versions()" + " to view for available versions and solcx.install_solc() to install." + ) + return _default_solc_binary + + version = _convert_and_validate_version(version) + solc_bin = get_solcx_install_folder(solcx_binary_path).joinpath(f"solc-v{version}") + if _get_os_name() == "windows": solc_bin = solc_bin.joinpath("solc.exe") if not solc_bin.exists(): raise SolcNotInstalled( - "solc {} has not been installed. ".format(version) - + "Use solcx.install_solc('{}') to install.".format(version) + f"solc {version} has not been installed." + f" Use solcx.install_solc('{version}') to install." ) - return str(solc_bin) + return solc_bin + + +def set_solc_version( + version: Union[str, Version], silent: bool = False, solcx_binary_path: Union[Path, str] = None +) -> None: + """ + Set the currently active `solc` binary. + + Arguments + --------- + version : str | Version, optional + Installed `solc` version to get the path of. If not given, returns the + path of the active version. + silent : bool, optional + If True, do not generate any logger output. + solcx_binary_path : Path | str, optional + User-defined path, used to override the default installation directory. + """ + version = _convert_and_validate_version(version) + global _default_solc_binary + _default_solc_binary = get_executable(version, solcx_binary_path) + if not silent: + LOGGER.info(f"Using solc version {version}") -def set_solc_version(version, silent=False, solcx_binary_path=None): - version = _check_version(version) - get_executable(version, solcx_binary_path) - global solc_version - solc_version = version - if not silent: - LOGGER.info("Using solc version {}".format(solc_version)) +def _select_pragma_version(pragma_string: str, version_list: List[Version]) -> Optional[Version]: + comparator_set_range = pragma_string.replace(" ", "").split("||") + comparator_regex = re.compile(r"(([<>]?=?|\^)\d+\.\d+\.\d+)+") + version = None + for comparator_set in comparator_set_range: + spec = SimpleSpec(*(i[0] for i in comparator_regex.findall(comparator_set))) + selected = spec.select(version_list) + if selected and (not version or version < selected): + version = selected -def set_solc_version_pragma(pragma_string, silent=False, check_new=False): - version = _select_pragma_version( - pragma_string, [Version(i[1:]) for i in get_installed_solc_versions()] - ) - if not version: + return version + + +def set_solc_version_pragma( + pragma_string: str, silent: bool = False, check_new: bool = False +) -> Version: + """ + Set the currently active `solc` binary based on a pragma statement. + + The newest installed version that matches the pragma is chosen. Raises + `SolcNotInstalled` if no installed versions match. + + Arguments + --------- + pragma_string : str + Pragma statement, e.g. "pragma solidity ^0.4.22;" + silent : bool, optional + If True, do not generate any logger output. + check_new : bool, optional + If True, also check if there is a newer compatible version that has not + been installed. + + Returns + ------- + Version + The new active `solc` version. + """ + version = _select_pragma_version(pragma_string, get_installed_solc_versions()) + if version is None: raise SolcNotInstalled( - "No compatible solc version installed. " - + "Use solcx.install_solc_version_pragma('{}') to install.".format(version) + f"No compatible solc version installed." + f" Use solcx.install_solc_version_pragma('{version}') to install." ) - version = _check_version(version) - global solc_version - solc_version = version - if not silent: - LOGGER.info("Using solc version {}".format(solc_version)) + set_solc_version(version, silent) if check_new: latest = install_solc_pragma(pragma_string, False) - if Version(latest) > Version(version[1:]): - LOGGER.info("Newer compatible solc version exists: {}".format(latest)) + if latest > version: + LOGGER.info(f"Newer compatible solc version exists: {latest}") + + return version -def install_solc_pragma(pragma_string, install=True, show_progress=False, solcx_binary_path=None): - version = _select_pragma_version( - pragma_string, [Version(i[1:]) for i in get_available_solc_versions()] - ) +def install_solc_pragma( + pragma_string: str, + install: bool = True, + show_progress: bool = False, + solcx_binary_path: Union[Path, str] = None, +) -> Version: + """ + Find, and optionally install, the latest compatible `solc` version based on + a pragma statement. + + Arguments + --------- + pragma_string : str + Pragma statement, e.g. "pragma solidity ^0.4.22;" + install : bool, optional + If True, installs the version of `solc`. + show_progress : bool, optional + If True, display a progress bar while downloading. Requires installing + the `tqdm` package. + solcx_binary_path : Path | str, optional + User-defined path, used to override the default installation directory. + + Returns + ------- + Version + Installed `solc` version. + """ + version = _select_pragma_version(pragma_string, get_installable_solc_versions()) if not version: - raise ValueError("Compatible solc version does not exist") + raise UnsupportedVersionError("Compatible solc version does not exist") if install: install_solc(version, show_progress=show_progress, solcx_binary_path=solcx_binary_path) + return version -def get_available_solc_versions(headers=None): - versions = [] - pattern = VERSION_REGEX[_get_platform()] +def get_installable_solc_versions() -> List[Version]: + """ + Return a list of all `solc` versions that can be installed by py-solc-x. + + Returns + ------- + List + List of Versions objects of installable `solc` versions. + """ + data = requests.get(BINARY_DOWNLOAD_BASE.format(_get_os_name(), "list.json")) + if data.status_code != 200: + raise ConnectionError( + f"Status {data.status_code} when getting solc versions from solc-bin.ethereum.org" + ) + version_list = sorted((Version(i) for i in data.json()["releases"]), reverse=True) + version_list = [i for i in version_list if i >= MINIMAL_SOLC_VERSION] + return version_list + + +def get_compilable_solc_versions(headers: Optional[Dict] = None) -> List[Version]: + """ + Return a list of all `solc` versions that can be compiled from source by py-solc-x. + + Arguments + --------- + headers : Dict, optional + Headers to include in the request to Github. + + Returns + ------- + List + List of Versions objects of installable `solc` versions. + """ + if _get_os_name() == "windows": + raise OSError("Compiling from source is not supported on Windows systems") + + version_list = [] + pattern = "solidity_[0-9].[0-9].[0-9]{1,}.tar.gz" - if not headers and os.getenv("GITHUB_TOKEN"): - auth = b64encode(os.getenv("GITHUB_TOKEN").encode()).decode() - headers = {"Authorization": "Basic {}".format(auth)} + if headers is None and os.getenv("GITHUB_TOKEN") is not None: + auth = b64encode(os.environ["GITHUB_TOKEN"].encode()).decode() + headers = {"Authorization": f"Basic {auth}"} - data = requests.get(ALL_RELEASES, headers=headers) + data = requests.get(GITHUB_RELEASES, headers=headers) if data.status_code != 200: - msg = "Status {} when getting solc versions from Github: '{}'".format( - data.status_code, data.json()["message"] + msg = ( + f"Status {data.status_code} when getting solc versions from Github:" + f" '{data.json()['message']}'" ) if data.status_code == 403: msg += ( @@ -197,105 +368,201 @@ def get_available_solc_versions(headers=None): raise ConnectionError(msg) for release in data.json(): + version = Version.coerce(release["tag_name"].lstrip("v")) asset = next((i for i in release["assets"] if re.match(pattern, i["name"])), False) if asset: - versions.append(release["tag_name"]) - if release["tag_name"] == MINIMAL_SOLC_VERSION: + version_list.append(version) + if version == MINIMAL_SOLC_VERSION: break - return versions + return sorted(version_list, reverse=True) + + +def get_installed_solc_versions(solcx_binary_path: Union[Path, str] = None) -> List[Version]: + """ + Return a list of currently installed `solc` versions. + + Arguments + --------- + solcx_binary_path : Path | str, optional + User-defined path, used to override the default installation directory. + + Returns + ------- + List + List of Version objects of installed `solc` versions. + """ + install_path = get_solcx_install_folder(solcx_binary_path) + return sorted([Version(i.name[6:]) for i in install_path.glob("solc-v*")], reverse=True) + + +def install_solc( + version: Union[str, Version] = "latest", + show_progress: bool = False, + solcx_binary_path: Union[Path, str] = None, +) -> Version: + """ + Download and install a precompiled version of `solc`. + + Arguments + --------- + version : str | Version, optional + Version of `solc` to install. Default is the newest available version. + show_progress : bool, optional + If True, display a progress bar while downloading. Requires installing + the `tqdm` package. + solcx_binary_path : Path | str, optional + User-defined path, used to override the default installation directory. + + Returns + ------- + Version + installed solc version + """ + + if version == "latest": + version = get_installable_solc_versions()[0] + else: + version = _convert_and_validate_version(version) + os_name = _get_os_name() + process_lock = get_process_lock(str(version)) -def _select_pragma_version(pragma_string, version_list): - comparator_set_range = pragma_string.replace(" ", "").split("||") - comparator_regex = re.compile(r"(([<>]?=?|\^)\d+\.\d+\.\d+)+") - version = None + with process_lock: + if _check_for_installed_version(version, solcx_binary_path): + path = get_solcx_install_folder(solcx_binary_path).joinpath(f"solc-v{version}") + LOGGER.info(f"solc {version} already installed at: {path}") + return version - for comparator_set in comparator_set_range: - spec = SimpleSpec(*(i[0] for i in comparator_regex.findall(comparator_set))) - selected = spec.select(version_list) - if selected and (not version or version < selected): - version = selected - if version: - return str(version) + data = requests.get(BINARY_DOWNLOAD_BASE.format(_get_os_name(), "list.json")) + if data.status_code != 200: + raise ConnectionError( + f"Status {data.status_code} when getting solc versions from solc-bin.ethereum.org" + ) + try: + filename = data.json()["releases"][str(version)] + except KeyError: + raise SolcInstallationError(f"Solc binary for v{version} is not available for this OS") + + if os_name == "linux": + _install_solc_unix(version, filename, show_progress, solcx_binary_path) + elif os_name == "macosx": + _install_solc_unix(version, filename, show_progress, solcx_binary_path) + elif os_name == "windows": + _install_solc_windows(version, filename, show_progress, solcx_binary_path) + try: + _validate_installation(version, solcx_binary_path) + except SolcInstallationError as exc: + exc.args = ( + f"{exc.args[0]} If this issue persists, you can try to compile from " + f"source code using `solcx.compile_solc('{version}')`.", + ) + raise exc -def get_installed_solc_versions(solcx_binary_path=None): - return sorted( - i.name[5:] for i in get_solc_folder(solcx_binary_path=solcx_binary_path).glob("solc-v*") - ) + return version -def install_solc(version, allow_osx=False, show_progress=False, solcx_binary_path=None): - arch = _get_arch() - platform = _get_platform() - version = _check_version(version) +def compile_solc( + version: Version, show_progress: bool = False, solcx_binary_path: Union[Path, str] = None +) -> Version: + """ + Install a version of `solc` by downloading and compiling source code. + + Arguments + --------- + version : str | Version, optional + Version of `solc` to install. Default is the newest available version. + show_progress : bool, optional + If True, display a progress bar while downloading. Requires installing + the `tqdm` package. + solcx_binary_path : Path | str, optional + User-defined path, used to override the default installation directory. + + Returns + ------- + Version + installed solc version + """ + if _get_os_name() == "windows": + raise OSError("Compiling from source is not supported on Windows systems") + + if version == "latest": + version = get_compilable_solc_versions()[0] + else: + version = _convert_and_validate_version(version) - lock = get_process_lock(version) - if not lock.acquire(False): - lock.wait() - if not _check_for_installed_version(version): - return - return install_solc(version, allow_osx) + process_lock = get_process_lock(str(version)) - try: - if arch == "arm": - _install_solc_arm(version, show_progress, solcx_binary_path) - elif platform == "linux": - _install_solc_linux(version, show_progress, solcx_binary_path) - elif platform == "darwin": - _install_solc_osx(version, allow_osx, show_progress, solcx_binary_path) - elif platform == "win32": - _install_solc_windows(version, show_progress, solcx_binary_path) - binary_path = get_executable(version, solcx_binary_path) - _check_subprocess_call( - [binary_path, "--version"], - message="Checking installed executable version @ {}".format(binary_path), - ) - if not solc_version: - set_solc_version(version) - LOGGER.info("solc {} successfully installed at: {}".format(version, binary_path)) - finally: - lock.release() + with process_lock: + if _check_for_installed_version(version, solcx_binary_path): + path = get_solcx_install_folder(solcx_binary_path).joinpath(f"solc-v{version}") + LOGGER.info(f"solc {version} already installed at: {path}") + return version + temp_path = _get_temp_folder() + download = SOURCE_DOWNLOAD_BASE.format(version, f"solidity_{version}.tar.gz") + install_path = get_solcx_install_folder(solcx_binary_path).joinpath(f"solc-v{version}") -def _check_version(version): - version = Version(version.lstrip("v")) - if version not in SimpleSpec(">=0.4.11"): - raise ValueError("py-solc-x does not support solc versions <0.4.11") - return "v" + str(version) + content = _download_solc(download, show_progress) + with tarfile.open(fileobj=BytesIO(content)) as tar: + tar.extractall(temp_path) + temp_path = temp_path.joinpath(f"solidity_{version}") + try: + LOGGER.info("Running dependency installation script `install_deps.sh`...") + subprocess.check_call( + ["sh", temp_path.joinpath("scripts/install_deps.sh")], stderr=subprocess.DEVNULL + ) + except subprocess.CalledProcessError as exc: + LOGGER.warning(exc, exc_info=True) -def _check_subprocess_call(command, message=None, verbose=False, **proc_kwargs): - if message: - LOGGER.debug(message) - LOGGER.info("Executing: {0}".format(" ".join(command))) + original_path = os.getcwd() + temp_path.joinpath("build").mkdir(exist_ok=True) + os.chdir(str(temp_path.joinpath("build").resolve())) + try: + for cmd in (["cmake", ".."], ["make"]): + LOGGER.info(f"Running `{cmd[0]}`...") + subprocess.check_call(cmd, stderr=subprocess.DEVNULL) + temp_path.joinpath("build/solc/solc").rename(install_path) + except subprocess.CalledProcessError as exc: + err_msg = ( + f"{cmd[0]} returned non-zero exit status {exc.returncode}" + " while attempting to build solc from the source.\n" + "This is likely due to a missing or incorrect version of a build dependency." + ) + if _get_os_name() == "macosx": + err_msg = ( + f"{err_msg}\n\nFor suggested installation options: " + "https://github.com/iamdefinitelyahuman/py-solc-x/wiki/Installing-Solidity-on-OSX" # noqa: E501 + ) + raise SolcInstallationError(err_msg) - return subprocess.check_call( - command, stderr=subprocess.STDOUT if verbose else subprocess.DEVNULL, **proc_kwargs - ) + finally: + os.chdir(original_path) + install_path.chmod(install_path.stat().st_mode | stat.S_IEXEC) + _validate_installation(version, solcx_binary_path) -def _chmod_plus_x(executable_path): - executable_path.chmod(executable_path.stat().st_mode | stat.S_IEXEC) + return version -def _check_for_installed_version(version, solcx_binary_path=None): - path = get_solc_folder(solcx_binary_path=solcx_binary_path).joinpath("solc-" + version) - if path.exists(): - LOGGER.info("solc {} already installed at: {}".format(version, path)) - return False - return path +def _check_for_installed_version( + version: Version, solcx_binary_path: Union[Path, str] = None +) -> bool: + path = get_solcx_install_folder(solcx_binary_path).joinpath(f"solc-v{version}") + return path.exists() -def _get_temp_folder(): - path = Path(tempfile.gettempdir()).joinpath("solcx-tmp-{}".format(os.getpid())) +def _get_temp_folder() -> Path: + path = Path(tempfile.gettempdir()).joinpath(f"solcx-tmp-{os.getpid()}") if path.exists(): shutil.rmtree(str(path)) path.mkdir() return path -def _download_solc(url, show_progress): +def _download_solc(url: str, show_progress: bool) -> bytes: + LOGGER.info(f"Downloading from {url}") response = requests.get(url, stream=show_progress) if response.status_code == 404: raise DownloadError( @@ -304,9 +571,7 @@ def _download_solc(url, show_progress): ) if response.status_code != 200: raise DownloadError( - "Received status code {} when attempting to download from {}".format( - response.status_code, url - ) + f"Received status code {response.status_code} when attempting to download from {url}" ) if not show_progress: return response.content @@ -323,86 +588,61 @@ def _download_solc(url, show_progress): return content -def _install_solc_linux(version, show_progress, solcx_binary_path=None): - download = DOWNLOAD_BASE.format(version, "solc-static-linux") - binary_path = _check_for_installed_version(version, solcx_binary_path=solcx_binary_path) - if binary_path: - LOGGER.info("Downloading solc {} from {}".format(version, download)) - content = _download_solc(download, show_progress) - with open(binary_path, "wb") as fp: - fp.write(content) - _chmod_plus_x(binary_path) - +def _install_solc_unix( + version: Version, filename: str, show_progress: bool, solcx_binary_path: Union[Path, str, None] +) -> None: + download = BINARY_DOWNLOAD_BASE.format(_get_os_name(), filename) + install_path = get_solcx_install_folder(solcx_binary_path).joinpath(f"solc-v{version}") -def _install_solc_windows(version, show_progress, solcx_binary_path=None): - download = DOWNLOAD_BASE.format(version, "solidity-windows.zip") - install_folder = _check_for_installed_version(version) - if install_folder: - temp_path = _get_temp_folder() - content = _download_solc(download, show_progress) - with zipfile.ZipFile(BytesIO(content)) as zf: - zf.extractall(str(temp_path)) - install_folder = get_solc_folder(solcx_binary_path=solcx_binary_path).joinpath( - "solc-" + version - ) - temp_path.rename(install_folder) - - -def _install_solc_arm(version, show_progress, solcx_binary_path): - _compile_solc(version, show_progress, solcx_binary_path) + content = _download_solc(download, show_progress) + with open(install_path, "wb") as fp: + fp.write(content) + install_path.chmod(install_path.stat().st_mode | stat.S_IEXEC) -def _install_solc_osx(version, allow_osx, show_progress, solcx_binary_path): - if version.startswith("v0.4") and not allow_osx: - raise ValueError( - "Installing solc {0} on OSX often fails. For suggested installation options:\n" - "https://github.com/iamdefinitelyahuman/py-solc-x/wiki/Installing-Solidity-on-OSX\n\n" - "To ignore this warning and attempt to install: " - "solcx.install_solc('{0}', allow_osx=True)".format(version) - ) - else: - _compile_solc(version, show_progress, solcx_binary_path) +def _install_solc_windows( + version: Version, filename: str, show_progress: bool, solcx_binary_path: Union[Path, str, None] +) -> None: + download = BINARY_DOWNLOAD_BASE.format(_get_os_name(), filename) + install_path = get_solcx_install_folder(solcx_binary_path).joinpath(f"solc-v{version}") -def _compile_solc(version, show_progress, solcx_binary_path): temp_path = _get_temp_folder() - download = DOWNLOAD_BASE.format(version, "solidity_{}.tar.gz".format(version[1:])) - binary_path = _check_for_installed_version(version) - if not binary_path: - return - content = _download_solc(download, show_progress) - with tarfile.open(fileobj=BytesIO(content)) as tar: - tar.extractall(temp_path) - temp_path = temp_path.joinpath("solidity_{}".format(version[1:])) + with zipfile.ZipFile(BytesIO(content)) as zf: + zf.extractall(str(temp_path)) + + temp_path.rename(install_path) - try: - _check_subprocess_call( - ["sh", str(temp_path.joinpath("scripts/install_deps.sh"))], - message="Running dependency installation script `install_deps.sh`", - ) - except subprocess.CalledProcessError as e: - LOGGER.warning(e, exc_info=True) - original_path = os.getcwd() - temp_path.joinpath("build").mkdir(exist_ok=True) - os.chdir(str(temp_path.joinpath("build").resolve())) +def _validate_installation(version: Version, solcx_binary_path: Union[Path, str, None]) -> None: + binary_path = get_executable(version, solcx_binary_path) try: - for cmd in (["cmake", ".."], ["make"]): - _check_subprocess_call(cmd, message="Running {}".format(cmd[0])) - temp_path.joinpath("build/solc/solc").rename(binary_path) - except subprocess.CalledProcessError as e: - raise OSError( - "{} returned non-zero exit status {} while attempting to build solc from the source.\n" - "This is likely due to a missing or incorrect version of a build dependency.\n\n" - "For suggested installation options: " - "https://github.com/iamdefinitelyahuman/py-solc-x/wiki/Installing-Solidity-on-OSX" - "".format(cmd[0], e.returncode) + installed_version = wrapper._get_solc_version(binary_path) + except Exception: + _unlink_solc(binary_path) + raise SolcInstallationError( + "Downloaded binary would not execute, or returned unexpected output." ) - finally: - os.chdir(original_path) + if installed_version.truncate() != version.truncate(): + _unlink_solc(binary_path) + raise UnexpectedVersionError( + f"Attempted to install solc v{version}, but got solc v{installed_version}" + ) + if installed_version != version: + warnings.warn(f"Installed solc version is v{installed_version}", UnexpectedVersionWarning) + if not _default_solc_binary: + set_solc_version(version) + LOGGER.info(f"solc {version} successfully installed at: {binary_path}") + - _chmod_plus_x(binary_path) +try: + # try to set the result of `which`/`where` as the default + _default_solc_binary = _get_which_solc() +except Exception: + # if not available, use the most recent solcx installed version + if get_installed_solc_versions(): + set_solc_version(get_installed_solc_versions()[0], silent=True) if __name__ == "__main__": diff --git a/solcx/main.py b/solcx/main.py index ad21b40..c579672 100644 --- a/solcx/main.py +++ b/solcx/main.py @@ -1,57 +1,244 @@ -from __future__ import absolute_import - -import functools import json -import re - -import semantic_version +from pathlib import Path +from typing import Any, Dict, List, Optional, Union -from .exceptions import ContractsNotFound, SolcError -from .install import get_executable -from .utils.filesystem import is_executable_available -from .wrapper import solc_wrapper +from semantic_version import Version -VERSION_DEV_DATE_MANGLER_RE = re.compile(r"(\d{4})\.0?(\d{1,2})\.0?(\d{1,2})") -strip_zeroes_from_month_and_day = functools.partial( - VERSION_DEV_DATE_MANGLER_RE.sub, r"\g<1>.\g<2>.\g<3>" -) +from solcx import wrapper +from solcx.exceptions import ContractsNotFound, SolcError +from solcx.install import get_executable +# from solcx.wrapper import _get_solc_version, solc_wrapper -def is_solc_available(): - solc_binary = get_executable() - return is_executable_available(solc_binary) +def get_solc_version() -> Version: + """ + Get the version of the active `solc` binary. -def get_solc_version_string(**kwargs): - kwargs["version"] = True - stdoutdata, stderrdata, command, proc = solc_wrapper(**kwargs) - _, _, version_string = stdoutdata.partition("\n") - if not version_string or not version_string.startswith("Version: "): - raise SolcError( - command=command, - return_code=proc.returncode, - stdin_data=None, - stdout_data=stdoutdata, - stderr_data=stderrdata, - message="Unable to extract version string from command output", - ) - return version_string.rstrip() + Returns + ------- + Version + solc version + """ + solc_binary = get_executable() + return wrapper._get_solc_version(solc_binary) + + +def compile_source( + source: str, + output_values: List = None, + import_remappings: Union[Dict, List, str] = None, + base_path: Union[Path, str] = None, + allow_paths: Union[List, Path, str] = None, + output_dir: Union[Path, str] = None, + overwrite: bool = False, + evm_version: str = None, + revert_strings: Union[List, str] = None, + metadata_hash: str = None, + metadata_literal: bool = False, + optimize: bool = False, + optimize_runs: int = None, + optimize_yul: bool = False, + no_optimize_yul: bool = False, + yul_optimizations: int = None, + solc_binary: Union[str, Path] = None, + solc_version: Version = None, + allow_empty: bool = False, +) -> Dict: + """ + Compile a Solidity contract. + + Compilation is handled via the `--combined-json` flag. Depending on the solc + version used, some keyword arguments may not be available. + + Arguments + --------- + source: str + Solidity contract to be compiled. + output_values : List, optional + Compiler outputs to return. Valid options depend on the version of `solc`. + If not given, all possible outputs for the active version are returned. + import_remappings : Dict | List | str , optional + Path remappings. May be given as a string or list of strings formatted as + `"prefix=path"`, or a dict of `{"prefix": "path"}`. + base_path : Path | str, optional + Use the given path as the root of the source tree instead of the root + of the filesystem. + allow_paths : List | Path | str, optional + A path, or list of paths, to allow for imports. + output_dir : str, optional + Creates one file per component and contract/file at the specified directory. + overwrite : bool, optional + Overwrite existing files (used in combination with `output_dir`) + evm_version: str, optional + Select the desired EVM version. Valid options depend on the `solc` version. + revert_strings : List | str, optional + Strip revert (and require) reason strings or add additional debugging + information. + metadata_hash : str, optional + Choose hash method for the bytecode metadata or disable it. + metadata_literal : bool, optional + Store referenced sources as literal data in the metadata output. + optimize : bool, optional + Enable bytecode optimizer. + optimize_runs : int, optional + Set for how many contract runs to optimize. Lower values will optimize + more for initial deployment cost, higher values will optimize more for + high-frequency usage. + optimize_yul: bool, optional + Enable the yul optimizer. + no_optimize_yul : bool, optional + Disable the yul optimizer. + yul_optimizations : int, optional + Force yul optimizer to use the specified sequence of optimization steps + instead of the built-in one. + solc_binary : str | Path, optional + Path of the `solc` binary to use. If not given, the currently active + version is used (as set by `solcx.set_solc_version`) + solc_version: Version, optional + `solc` version to use. If not given, the currently active version is used. + Ignored if `solc_binary` is also given. + allow_empty : bool, optional + If `True`, do not raise when no compiled contracts are returned. + + Returns + ------- + Dict + Compiler output. The source file name is given as ``. + """ + return _compile_combined_json( + solc_binary=solc_binary, + solc_version=solc_version, + stdin=source, + output_values=output_values, + import_remappings=import_remappings, + base_path=base_path, + allow_paths=allow_paths, + output_dir=output_dir, + overwrite=overwrite, + evm_version=evm_version, + revert_strings=revert_strings, + metadata_hash=metadata_hash, + metadata_literal=metadata_literal, + optimize=optimize, + optimize_runs=optimize_runs, + no_optimize_yul=no_optimize_yul, + yul_optimizations=yul_optimizations, + allow_empty=allow_empty, + ) -def get_solc_version(**kwargs): - # semantic_version as of 2017-5-5 expects only one + to be used in string - return semantic_version.Version( - strip_zeroes_from_month_and_day( - get_solc_version_string(**kwargs)[len("Version: ") :].replace("++", "pp") - ) +def compile_files( + source_files: Union[List, Path, str], + output_values: List = None, + import_remappings: Union[Dict, List, str] = None, + base_path: Union[Path, str] = None, + allow_paths: Union[List, Path, str] = None, + output_dir: Union[Path, str] = None, + overwrite: bool = False, + evm_version: str = None, + revert_strings: Union[List, str] = None, + metadata_hash: str = None, + metadata_literal: bool = False, + optimize: bool = False, + optimize_runs: int = None, + optimize_yul: bool = False, + no_optimize_yul: bool = False, + yul_optimizations: int = None, + solc_binary: Union[str, Path] = None, + solc_version: Version = None, + allow_empty: bool = False, +) -> Dict: + """ + Compile one or more Solidity source files. + + Compilation is handled via the `--combined-json` flag. Depending on the solc + version used, some keyword arguments may not be available. + + Arguments + --------- + source_files: List | Path | str + Path, or list of paths, of Solidity source files to be compiled. + output_values : List, optional + Compiler outputs to return. Valid options depend on the version of `solc`. + If not given, all possible outputs for the active version are returned. + import_remappings : Dict | List | str , optional + Path remappings. May be given as a string or list of strings formatted as + `"prefix=path"`, or a dict of `{"prefix": "path"}`. + base_path : Path | str, optional + Use the given path as the root of the source tree instead of the root + of the filesystem. + allow_paths : List | Path | str, optional + A path, or list of paths, to allow for imports. + output_dir : str, optional + Creates one file per component and contract/file at the specified directory. + overwrite : bool, optional + Overwrite existing files (used in combination with `output_dir`) + evm_version: str, optional + Select the desired EVM version. Valid options depend on the `solc` version. + revert_strings : List | str, optional + Strip revert (and require) reason strings or add additional debugging + information. + metadata_hash : str, optional + Choose hash method for the bytecode metadata or disable it. + metadata_literal : bool, optional + Store referenced sources as literal data in the metadata output. + optimize : bool, optional + Enable bytecode optimizer. + optimize_runs : int, optional + Set for how many contract runs to optimize. Lower values will optimize + more for initial deployment cost, higher values will optimize more for + high-frequency usage. + optimize_yul: bool, optional + Enable the yul optimizer. + no_optimize_yul : bool, optional + Disable the yul optimizer. + yul_optimizations : int, optional + Force yul optimizer to use the specified sequence of optimization steps + instead of the built-in one. + solc_binary : str | Path, optional + Path of the `solc` binary to use. If not given, the currently active + version is used (as set by `solcx.set_solc_version`) + solc_version: Version, optional + `solc` version to use. If not given, the currently active version is used. + Ignored if `solc_binary` is also given. + allow_empty : bool, optional + If `True`, do not raise when no compiled contracts are returned. + + Returns + ------- + Dict + Compiler output + """ + return _compile_combined_json( + solc_binary=solc_binary, + solc_version=solc_version, + source_files=source_files, + output_values=output_values, + import_remappings=import_remappings, + base_path=base_path, + allow_paths=allow_paths, + output_dir=output_dir, + overwrite=overwrite, + evm_version=evm_version, + revert_strings=revert_strings, + metadata_hash=metadata_hash, + metadata_literal=metadata_literal, + optimize=optimize, + optimize_runs=optimize_runs, + no_optimize_yul=no_optimize_yul, + yul_optimizations=yul_optimizations, + allow_empty=allow_empty, ) -def solc_supports_standard_json_interface(**kwargs): - return get_solc_version() in semantic_version.SimpleSpec(">=0.4.11") +def _get_combined_json_outputs() -> str: + help_str = wrapper.solc_wrapper(help=True)[0].split("\n") + combined_json_args = next(i for i in help_str if i.startswith(" --combined-json")) + return combined_json_args.split(" ")[-1] -def _parse_compiler_output(stdoutdata): +def _parse_compiler_output(stdoutdata: str) -> Dict: output = json.loads(stdoutdata) contracts = output.get("contracts", {}) @@ -67,55 +254,51 @@ def _parse_compiler_output(stdoutdata): return contracts -ALL_OUTPUT_VALUES = ( - "abi", - "asm", - "ast", - "bin", - "bin-runtime", - "clone-bin", - "devdoc", - "opcodes", - "userdoc", -) - - -def compile_source(source, allow_empty=False, output_values=ALL_OUTPUT_VALUES, **kwargs): - if "stdin" in kwargs: - raise ValueError("The `stdin` keyword is not allowed in the `compile_source` function") - if "combined_json" in kwargs: - raise ValueError( - "The `combined_json` keyword is not allowed in the `compile_source` function" - ) - - combined_json = ",".join(output_values) - compiler_kwargs = dict(stdin=source, combined_json=combined_json, **kwargs) - - stdoutdata, stderrdata, command, proc = solc_wrapper(**compiler_kwargs) - - contracts = _parse_compiler_output(stdoutdata) - - if not contracts and not allow_empty: - raise ContractsNotFound( - command=command, - return_code=proc.returncode, - stdin_data=source, - stdout_data=stdoutdata, - stderr_data=stderrdata, - ) - return contracts - - -def compile_files(source_files, allow_empty=False, output_values=ALL_OUTPUT_VALUES, **kwargs): - if "combined_json" in kwargs: - raise ValueError( - "The `combined_json` keyword is not allowed in the `compile_files` function" - ) +def _compile_combined_json( + output_values: Optional[List], + solc_binary: Union[str, Path, None], + solc_version: Optional[Version], + output_dir: Union[str, Path, None], + overwrite: Optional[bool], + allow_empty: Optional[bool], + **kwargs: Any, +) -> Dict: + + if output_values is None: + combined_json = _get_combined_json_outputs() + else: + combined_json = ",".join(output_values) + + if solc_binary is None: + solc_binary = get_executable(solc_version) + + if output_dir: + output_dir = Path(output_dir) + if output_dir.is_file(): + raise FileExistsError("`output_dir` must be as a directory, not a file") + if output_dir.joinpath("combined.json").exists() and not overwrite: + target_path = output_dir.joinpath("combined.json") + raise FileExistsError( + f"Target output file {target_path} already exists - use overwrite=True to overwrite" + ) - combined_json = ",".join(output_values) - compiler_kwargs = dict(source_files=source_files, combined_json=combined_json, **kwargs) + stdoutdata, stderrdata, command, proc = wrapper.solc_wrapper( + solc_binary=solc_binary, + combined_json=combined_json, + output_dir=output_dir, + overwrite=overwrite, + **kwargs, + ) - stdoutdata, stderrdata, command, proc = solc_wrapper(**compiler_kwargs) + if output_dir: + output_path = Path(output_dir).joinpath("combined.json") + if stdoutdata: + output_path.parent.mkdir(parents=True, exist_ok=True) + with output_path.open("w") as fp: + fp.write(stdoutdata) + else: + with output_path.open() as fp: + stdoutdata = fp.read() contracts = _parse_compiler_output(stdoutdata) @@ -123,25 +306,72 @@ def compile_files(source_files, allow_empty=False, output_values=ALL_OUTPUT_VALU raise ContractsNotFound( command=command, return_code=proc.returncode, - stdin_data=None, stdout_data=stdoutdata, stderr_data=stderrdata, ) return contracts -def compile_standard(input_data, allow_empty=False, **kwargs): +def compile_standard( + input_data: Dict, + base_path: str = None, + allow_paths: List = None, + output_dir: str = None, + overwrite: bool = False, + solc_binary: Union[str, Path] = None, + solc_version: Version = None, + allow_empty: bool = False, +) -> Dict: + """ + Compile Solidity contracts using the JSON-input-output interface. + + See the Solidity documentation for details on the expected JSON input and output + formats. + + Arguments + --------- + input_data : Dict + Compiler JSON input. + base_path : Path | str, optional + Use the given path as the root of the source tree instead of the root + of the filesystem. + allow_paths : List | Path | str, optional + A path, or list of paths, to allow for imports. + output_dir : str, optional + Creates one file per component and contract/file at the specified directory. + overwrite : bool, optional + Overwrite existing files (used in combination with `output_dir`) + solc_binary : str | Path, optional + Path of the `solc` binary to use. If not given, the currently active + version is used (as set by `solcx.set_solc_version`) + solc_version: Version, optional + `solc` version to use. If not given, the currently active version is used. + Ignored if `solc_binary` is also given. + allow_empty : bool, optional + If `True`, do not raise when no compiled contracts are returned. + + Returns + ------- + Dict + Compiler JSON output. + """ if not input_data.get("sources") and not allow_empty: raise ContractsNotFound( - command=None, - return_code=None, + "Input JSON does not contain any sources", stdin_data=json.dumps(input_data, sort_keys=True, indent=2), - stdout_data=None, - stderr_data=None, ) - stdoutdata, stderrdata, command, proc = solc_wrapper( - stdin=json.dumps(input_data), standard_json=True, **kwargs + if solc_binary is None: + solc_binary = get_executable(solc_version) + + stdoutdata, stderrdata, command, proc = wrapper.solc_wrapper( + solc_binary=solc_binary, + stdin=json.dumps(input_data), + standard_json=True, + base_path=base_path, + allow_paths=allow_paths, + output_dir=output_dir, + overwrite=overwrite, ) compiler_output = json.loads(stdoutdata) @@ -156,22 +386,51 @@ def compile_standard(input_data, allow_empty=False, **kwargs): ) ) raise SolcError( - command, - proc.returncode, - json.dumps(input_data), - stdoutdata, - stderrdata, - message=error_message, + error_message, + command=command, + return_code=proc.returncode, + stdin_data=json.dumps(input_data), + stdout_data=stdoutdata, + stderr_data=stderrdata, + error_dict=compiler_output["errors"], ) return compiler_output -def link_code(unlinked_bytecode, libraries): - libraries_arg = ",".join( - (":".join((lib_name, lib_address)) for lib_name, lib_address in libraries.items()) - ) - stdoutdata, stderrdata, _, _ = solc_wrapper( - stdin=unlinked_bytecode, link=True, libraries=libraries_arg, - ) +def link_code( + unlinked_bytecode: str, + libraries: Dict, + solc_binary: Union[str, Path] = None, + solc_version: Version = None, +) -> str: + """ + Add library addresses into unlinked bytecode. + + Arguments + --------- + unlinked_bytecode : str + Compiled bytecode containing one or more library placeholders. + libraries : Dict + Library addresses given as {"library name": "address"} + solc_binary : str | Path, optional + Path of the `solc` binary to use. If not given, the currently active + version is used (as set by `solcx.set_solc_version`) + solc_version: Version, optional + `solc` version to use. If not given, the currently active version is used. + Ignored if `solc_binary` is also given. + + Returns + ------- + str + Linked bytecode + """ + if solc_binary is None: + solc_binary = get_executable(solc_version) + + library_list = [f"{name}:{address}" for name, address in libraries.items()] + + stdoutdata = wrapper.solc_wrapper( + solc_binary=solc_binary, stdin=unlinked_bytecode, link=True, libraries=library_list + )[0] return stdoutdata.replace("Linking completed.", "").strip() diff --git a/solcx/utils/filesystem.py b/solcx/utils/filesystem.py deleted file mode 100644 index 0f3bcf7..0000000 --- a/solcx/utils/filesystem.py +++ /dev/null @@ -1,19 +0,0 @@ -import os - - -def is_executable_available(program): - def is_exe(fpath): - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - - fpath = os.path.dirname(program) - if fpath: - if is_exe(program): - return True - else: - for path in os.environ["PATH"].split(os.pathsep): - path = path.strip('"') - exe_file = os.path.join(path, program) - if is_exe(exe_file): - return True - - return False diff --git a/solcx/utils/lock.py b/solcx/utils/lock.py index c80b8f9..6e77dd2 100644 --- a/solcx/utils/lock.py +++ b/solcx/utils/lock.py @@ -3,7 +3,7 @@ import tempfile import threading from pathlib import Path -from typing import Dict +from typing import Any, Dict, Union if sys.platform == "win32": import msvcrt @@ -15,11 +15,11 @@ NON_BLOCKING = fcntl.LOCK_EX | fcntl.LOCK_NB BLOCKING = fcntl.LOCK_EX -_locks: Dict = {} +_locks: Dict[str, Union["UnixLock", "WindowsLock"]] = {} _base_lock = threading.Lock() -def get_process_lock(lock_id): +def get_process_lock(lock_id: str) -> Union["UnixLock", "WindowsLock"]: with _base_lock: if lock_id not in _locks: if sys.platform == "win32": @@ -30,18 +30,24 @@ def get_process_lock(lock_id): class _ProcessLock: - def __init__(self, lock_id): + """ + Ensure an action is both thread-safe and process-safe. + """ + + def __init__(self, lock_id: str) -> None: self._lock = threading.Lock() self._lock_path = Path(tempfile.gettempdir()).joinpath(f".solcx-lock-{lock_id}") self._lock_file = self._lock_path.open("w") - def wait(self): + +class UnixLock(_ProcessLock): + def __enter__(self) -> None: self.acquire(True) - self.release() + def __exit__(self, *args: Any) -> None: + self.release() -class UnixLock(_ProcessLock): - def acquire(self, blocking): + def acquire(self, blocking: bool) -> bool: if not self._lock.acquire(blocking): return False try: @@ -51,19 +57,27 @@ def acquire(self, blocking): return False return True - def release(self): + def release(self) -> None: fcntl.flock(self._lock_file, fcntl.LOCK_UN) self._lock.release() class WindowsLock(_ProcessLock): - def acquire(self, blocking): + def __enter__(self) -> None: + self.acquire(True) + + def __exit__(self, *args: Any) -> None: + self.release() + + def acquire(self, blocking: bool) -> bool: if not self._lock.acquire(blocking): return False while True: try: - fd = os.open(self._lock_path, OPEN_MODE) - msvcrt.locking(fd, msvcrt.LK_LOCK if blocking else msvcrt.LK_NBLCK, 1) + fd = os.open(self._lock_path, OPEN_MODE) # type: ignore + msvcrt.locking( # type: ignore + fd, msvcrt.LK_LOCK if blocking else msvcrt.LK_NBLCK, 1 # type: ignore + ) self._fd = fd return True except OSError: @@ -71,6 +85,6 @@ def acquire(self, blocking): self._lock.release() return False - def release(self): - msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1) + def release(self) -> None: + msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1) # type: ignore self._lock.release() diff --git a/solcx/utils/string.py b/solcx/utils/string.py deleted file mode 100644 index c584835..0000000 --- a/solcx/utils/string.py +++ /dev/null @@ -1,80 +0,0 @@ -import codecs -import functools - -from .types import is_bytes, is_dict, is_list_like, is_string, is_text - - -def force_bytes(value, encoding="iso-8859-1"): - if is_bytes(value): - return bytes(value) - elif is_text(value): - return codecs.encode(value, encoding) - else: - raise TypeError("Unsupported type: {0}".format(type(value))) - - -def force_text(value, encoding="iso-8859-1"): - if is_text(value): - return value - elif is_bytes(value): - return codecs.decode(value, encoding) - else: - raise TypeError("Unsupported type: {0}".format(type(value))) - - -def force_obj_to_bytes(obj): - if is_string(obj): - return force_bytes(obj) - elif is_dict(obj): - return {k: force_obj_to_bytes(v) for k, v in obj.items()} - elif is_list_like(obj): - return type(obj)(force_obj_to_bytes(v) for v in obj) - else: - return obj - - -def force_obj_to_text(obj): - if is_string(obj): - return force_text(obj) - elif is_dict(obj): - return {k: force_obj_to_text(v) for k, v in obj.items()} - elif is_list_like(obj): - return type(obj)(force_obj_to_text(v) for v in obj) - else: - return obj - - -def coerce_args_to_bytes(fn): - @functools.wraps(fn) - def inner(*args, **kwargs): - bytes_args = force_obj_to_bytes(args) - bytes_kwargs = force_obj_to_bytes(kwargs) - return fn(*bytes_args, **bytes_kwargs) - - return inner - - -def coerce_args_to_text(fn): - @functools.wraps(fn) - def inner(*args, **kwargs): - text_args = force_obj_to_text(args) - text_kwargs = force_obj_to_text(kwargs) - return fn(*text_args, **text_kwargs) - - return inner - - -def coerce_return_to_bytes(fn): - @functools.wraps(fn) - def inner(*args, **kwargs): - return force_obj_to_bytes(fn(*args, **kwargs)) - - return inner - - -def coerce_return_to_text(fn): - @functools.wraps(fn) - def inner(*args, **kwargs): - return force_obj_to_text(fn(*args, **kwargs)) - - return inner diff --git a/solcx/utils/types.py b/solcx/utils/types.py deleted file mode 100644 index c7dfe53..0000000 --- a/solcx/utils/types.py +++ /dev/null @@ -1,38 +0,0 @@ -import numbers -from collections import abc - - -def is_integer(value): - return isinstance(value, int) and not isinstance(value, bool) - - -def is_bytes(value): - return isinstance(value, (bytes, bytearray)) - - -def is_text(value): - return isinstance(value, str) - - -def is_string(value): - return isinstance(value, (bytes, str, bytearray)) - - -def is_boolean(value): - return isinstance(value, bool) - - -def is_dict(obj): - return isinstance(obj, abc.Mapping) - - -def is_list_like(obj): - return not is_string(obj) and isinstance(obj, abc.Sequence) - - -def is_null(obj): - return obj is None - - -def is_number(obj): - return isinstance(obj, numbers.Number) diff --git a/solcx/wrapper.py b/solcx/wrapper.py index 7bac364..d1bbf04 100644 --- a/solcx/wrapper.py +++ b/solcx/wrapper.py @@ -1,189 +1,149 @@ -from __future__ import absolute_import - +import re import subprocess +from pathlib import Path +from typing import Any, Dict, List, Tuple, Union from semantic_version import Version -from .exceptions import SolcError -from .install import get_executable -from .utils.string import coerce_return_to_text, force_bytes - - -@coerce_return_to_text -def solc_wrapper( - solc_binary=None, - stdin=None, - help=None, - version=None, - add_std=None, - combined_json=None, - optimize=None, - optimize_runs=None, - libraries=None, - output_dir=None, - gas=None, - assemble=None, - link=None, - source_files=None, - import_remappings=None, - ast=None, - ast_json=None, - asm=None, - asm_json=None, - opcodes=None, - bin=None, - bin_runtime=None, - clone_bin=None, - abi=None, - hashes=None, - userdoc=None, - devdoc=None, - formal=None, - allow_paths=None, - base_path=None, - standard_json=None, - success_return_code=None, - evm_version=None, -): - if solc_binary is None: - solc_binary = get_executable() - - command = [solc_binary] - - solc_version = Version(solc_binary.rsplit("-v")[-1].split("\\")[0]) - solc_minor = solc_version.minor - - if help: - command.append("--help") - if success_return_code is None: - success_return_code = 1 - elif success_return_code is None: - success_return_code = 0 - - if version: - command.append("--version") - - # removed in 0.4.21 and does nothing since <0.4.11, should be removed in the future - if add_std: - command.append("--add-std") - - if optimize: - command.append("--optimize") - - if optimize_runs is not None: - command.extend(("--optimize-runs", str(optimize_runs))) - - if link: - command.append("--link") - - if libraries is not None: - command.extend(("--libraries", libraries)) - - if output_dir is not None: - command.extend(("--output-dir", output_dir)) - - if combined_json: - if solc_minor >= 5: - combined_json = combined_json.replace(",clone-bin", "") - command.extend(("--combined-json", combined_json)) - - if gas: - command.append("--gas") - - if allow_paths: - command.extend(("--allow-paths", allow_paths)) - - if standard_json: - command.append("--standard-json") - - if assemble: - command.append("--assemble") - - if import_remappings is not None: - command.extend(import_remappings) - - if source_files is not None: - command.extend(source_files) - - # Output configuration - if ast_json: - command.append("--ast-json") - - if asm: - command.append("--asm") - - if asm_json: - command.append("--asm-json") +from solcx import install +from solcx.exceptions import SolcError, UnknownOption, UnknownValue - if opcodes: - command.append("--opcodes") - if bin: - command.append("--bin") +def _get_solc_version(solc_binary: Union[Path, str]) -> Version: + # private wrapper function to get `solc` version + stdout_data = subprocess.check_output([solc_binary, "--version"], encoding="utf8") + version_str = re.findall(r"(?<=Version: ).*?(?=\+)", stdout_data)[0] + version_str = re.sub(r"\.0(?=[1-9])", ".", version_str) + return Version.coerce(version_str) - if bin_runtime: - command.append("--bin-runtime") - if abi: - command.append("--abi") +def _to_string(key: str, value: Any) -> str: + # convert data into a string prior to calling `solc` + if isinstance(value, (int, str)): + return str(value) + elif isinstance(value, Path): + return value.as_posix() + elif isinstance(value, (list, tuple)): + return ",".join(_to_string(key, i) for i in value) + else: + raise TypeError(f"Invalid type for {key}: {type(value)}") - if hashes: - command.append("--hashes") - if userdoc: - command.append("--userdoc") - - if devdoc: - command.append("--devdoc") - - if evm_version: - command.extend(("--evm-version", evm_version)) - - # unsupported by <0.6.9 - if base_path: - if solc_version <= Version("0.6.8"): - raise AttributeError( - "solc {} does not support the --base-path flag".format(solc_version) - ) - command.extend(("--base-path", base_path)) - - # unsupported by >=0.6.0 - if ast: - if solc_minor >= 6: - raise AttributeError("solc 0.{}.x does not support the --ast flag".format(solc_minor)) - command.append("--ast") - - # unsupported by >=0.5.0 - if clone_bin: - if solc_minor >= 5: - raise AttributeError( - "solc 0.{}.x does not support the --clone-bin flag".format(solc_minor) - ) - command.append("--clone-bin") +def solc_wrapper( + solc_binary: Union[Path, str] = None, + stdin: str = None, + source_files: Union[List, Path, str] = None, + import_remappings: Union[Dict, List, str] = None, + success_return_code: int = None, + **kwargs: Any, +) -> Tuple[str, str, List, subprocess.Popen]: + """ + Wrapper function for calling to `solc`. + + Arguments + --------- + solc_binary : Path | str, optional + Location of the `solc` binary. If not given, the current default binary is used. + stdin : str, optional + Input to pass to `solc` via stdin + source_files : list | Path | str, optional + Path, or list of paths, of sources to compile + import_remappings : Dict | List | str, optional + Path remappings. May be given as a string or list of strings formatted as `"prefix=path"` + or a dict of `{"prefix": "path"}` + success_return_code : int, optional + Expected exit code. Raises `SolcError` if the process returns a different value. + + Keyword Arguments + ----------------- + **kwargs : Any + Flags to be passed to `solc`. Keywords are converted to flags by prepending `--` and + replacing `_` with `-`, for example the keyword `evm_version` becomes `--evm-version`. + Values may be given in the following formats: + + * `False`, `None`: ignored + * `True`: flag is used without any arguments + * str: given as an argument without modification + * int: given as an argument, converted to a string + * Path: converted to a string via `Path.as_posix()` + * List, Tuple: elements are converted to strings and joined with `,` + + Returns + ------- + str + Process `stdout` output + str + Process `stderr` output + List + Full command executed by the function + Popen + Subprocess object used to call `solc` + """ + if solc_binary: + solc_binary = Path(solc_binary) + else: + solc_binary = install.get_executable() + + solc_version = _get_solc_version(solc_binary) + command: List = [solc_binary] + + if success_return_code is None: + success_return_code = 1 if "help" in kwargs else 0 - if formal: - if solc_minor >= 5: - raise AttributeError( - "solc 0.{}.x does not support the --formal flag".format(solc_minor) - ) - command.append("--formal") + if source_files is not None: + if isinstance(source_files, (str, Path)): + command.append(_to_string("source_files", source_files)) + else: + command.extend([_to_string("source_files", i) for i in source_files]) - if not standard_json and not source_files: + if import_remappings is not None: + if isinstance(import_remappings, str): + command.append(import_remappings) + else: + if isinstance(import_remappings, dict): + import_remappings = [f"{k}={v}" for k, v in import_remappings.items()] + command.extend(import_remappings) + + for key, value in kwargs.items(): + if value is None or value is False: + continue + + key = f"--{key.replace('_', '-')}" + if value is True: + command.append(key) + else: + command.extend([key, _to_string(key, value)]) + + if "standard_json" not in kwargs and not source_files: # indicates that solc should read from stdin command.append("-") if stdin is not None: - # solc seems to expects utf-8 from stdin: - # see Scanner class in Solidity source - stdin = force_bytes(stdin, "utf8") + stdin = str(stdin) proc = subprocess.Popen( - command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf8", ) stdoutdata, stderrdata = proc.communicate(stdin) if proc.returncode != success_return_code: + if stderrdata.startswith("unrecognised option"): + # unrecognised option '' + flag = stderrdata.split("'")[1] + raise UnknownOption(f"solc {solc_version} does not support the '{flag}' option'") + if stderrdata.startswith("Invalid option"): + # Invalid option to :