Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,28 @@ The versioned binaries are stored in `~/.solc-select/artifacts/`.

## Installation

### Using pip
### Using uv

```bash
pip3 install solc-select
uv tool install solc-select
```

### Using uv (recommended for development)
### Using pip

```bash
uv tool install solc-select
pip3 install solc-select
```

To automatically install and use a version, run `solc-select use <version> --always-install`.

### Running on ARM (Mac M1/M2)
### Running on macOS ARM (Mac M1 and newer)

`solc-select` provides native ARM64 support for versions 0.8.5-0.8.23, and universal binary support for 0.8.24+. For versions older than 0.8.5, Rosetta is required. See the FAQ on [how to install Rosetta](#oserror-errno-86-bad-cpu-type-in-executable).

### Running on Linux ARM

`solc-select` provides native ARM64 support for versions 0.8.31+. For versions older than 0.8.31, QEMU (`qemu-x86_64`) is required. Additionally, a libc binary (e.g., from package `libc6-amd64-cross`) and adequate `QEMU_LD_PREFIX` environment variable (e.g., `QEMU_LD_PREFIX=/usr/x86_64-linux-gnu`) might be necessary to execute certain solc binaries that are not built statically.

## Usage

### Quick Start
Expand Down Expand Up @@ -160,7 +164,7 @@ pip3 install solc-select==0.2.0
solc-select install
```

### `solc-select` version changes, but `solc --version does not match`
### `solc-select` version changes, but `solc --version` does not match

Users seem to be experiencing situations in which the following command is successful:

Expand Down
44 changes: 16 additions & 28 deletions solc_select/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ def solc_select_install(service: SolcService, versions: list[str]) -> None:
"""Handle the install command."""
if not versions:
print("Available versions to install:")
installable = service.get_installable_versions()
for version in installable:
for version in service.get_installable_versions():
print(str(version))
else:
success = service.install_versions(versions)
Expand All @@ -42,21 +41,20 @@ def solc_select_use(service: SolcService, version: str, always_install: bool) ->
def solc_select_versions(service: SolcService) -> None:
"""Handle the versions command."""
installed = service.get_installed_versions()
if installed:
try:
current_version, source = service.get_current_version()
except (NoVersionSetError, VersionNotInstalledError):
# No version is currently set or not installed, that's ok for the versions command
current_version = None

installed_strs = [str(v) for v in installed]
for version_str in sort_versions(installed_strs):
if current_version and version_str == str(current_version):
print(f"{version_str} (current, set by {source})")
else:
print(version_str)
else:
if not installed:
print("No solc version installed. Run `solc-select install --help` for more information")
return

try:
current_version, source = service.get_current_version()
except (NoVersionSetError, VersionNotInstalledError):
current_version = None

for version_str in sort_versions([str(v) for v in installed]):
if current_version and version_str == str(current_version):
print(f"{version_str} (current, set by {source})")
else:
print(version_str)


def solc_select_upgrade(service: SolcService) -> None:
Expand All @@ -72,7 +70,6 @@ def create_parser() -> argparse.ArgumentParser:
dest="command",
)

# Install command
parser_install = subparsers.add_parser(
INSTALL_COMMAND, help="list and install available solc versions"
)
Expand All @@ -83,20 +80,17 @@ def create_parser() -> argparse.ArgumentParser:
default=[],
)

# Use command
parser_use = subparsers.add_parser(
USE_COMMAND, help="change the version of global solc compiler"
)
parser_use.add_argument("version", help="solc version you want to use (eg: 0.4.25)", nargs="?")
parser_use.add_argument("--always-install", action="store_true")

# Versions command
parser_versions = subparsers.add_parser(
VERSIONS_COMMAND, help="prints out all installed solc versions"
)
parser_versions.add_argument("versions", nargs="*", help=argparse.SUPPRESS)

# Upgrade command
parser_upgrade = subparsers.add_parser(UPGRADE_COMMAND, help="upgrades solc-select")
parser_upgrade.add_argument("upgrade", nargs="*", help=argparse.SUPPRESS)

Expand All @@ -106,25 +100,19 @@ def create_parser() -> argparse.ArgumentParser:
def solc_select() -> None:
parser = create_parser()
args = parser.parse_args()

# Create service instance
service = SolcService()

try:
if args.command == INSTALL_COMMAND:
solc_select_install(service, args.versions)

elif args.command == USE_COMMAND:
if not args.version:
parser.error("the following arguments are required: version")
solc_select_use(service, args.version, args.always_install)

elif args.command == VERSIONS_COMMAND:
solc_select_versions(service)

elif args.command == UPGRADE_COMMAND:
solc_select_upgrade(service)

else:
parser.parse_args(["--help"])
sys.exit(0)
Expand Down Expand Up @@ -154,7 +142,7 @@ def solc_select() -> None:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print("\nOperation cancelled by user", file=sys.stderr)
print("\nOperation cancelled", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Unexpected error: {e}", file=sys.stderr)
Expand All @@ -168,7 +156,7 @@ def solc() -> None:
try:
service.execute_solc(sys.argv[1:])
except KeyboardInterrupt:
print("\nOperation cancelled by user", file=sys.stderr)
print("\nOperation cancelled", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error executing solc: {e}", file=sys.stderr)
Expand Down
15 changes: 1 addition & 14 deletions solc_select/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
"""
Custom exception classes for solc-select.

This module provides a structured exception hierarchy for better error handling
and more informative error messages throughout the application.
"""
"""Custom exception classes for solc-select."""


class SolcSelectError(Exception):
"""Base exception for all solc-select errors."""

pass


class VersionNotFoundError(SolcSelectError):
"""Raised when requested version doesn't exist or isn't available."""
Expand All @@ -26,7 +19,6 @@ def __init__(
self.suggestion = suggestion

message = f"Version '{version}' not found"

if available_versions:
if len(available_versions) <= 5:
message += f". Available versions: {', '.join(available_versions)}"
Expand Down Expand Up @@ -77,7 +69,6 @@ def __init__(self, version: str, platform: str, min_version: str | None = None):
self.min_version = min_version

message = f"Version '{version}' is not supported on {platform}"

if min_version:
message += f". Minimum supported version is '{min_version}'"

Expand Down Expand Up @@ -105,7 +96,6 @@ class InstallationError(SolcSelectError):
def __init__(self, version: str, reason: str):
self.version = version
self.reason = reason

super().__init__(f"Failed to install version '{version}': {reason}")


Expand All @@ -120,10 +110,8 @@ def __init__(
self.original_error = original_error

message = f"Network error during {operation}"

if url:
message += f" from {url}"

if original_error:
message += f": {original_error!s}"

Expand All @@ -136,7 +124,6 @@ class VersionResolutionError(SolcSelectError):
def __init__(self, requested: str, reason: str):
self.requested = requested
self.reason = reason

super().__init__(f"Could not resolve version '{requested}': {reason}")


Expand Down
8 changes: 1 addition & 7 deletions solc_select/infrastructure/http_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
"""
HTTP client configuration for solc-select.

This module provides centralized HTTP client configuration with
retry logic and proper timeout handling.
"""
"""HTTP client configuration for solc-select."""

from collections.abc import Mapping
from typing import Any
Expand Down Expand Up @@ -38,7 +33,6 @@ def create_http_session() -> requests.Session:
"""Create a new HTTP session with retry logic for rate limits and server errors."""
session = requests.Session()

# Configure retry strategy for 429s and server errors
retry_strategy = Retry(
total=5,
backoff_factor=1,
Expand Down
4 changes: 1 addition & 3 deletions solc_select/models/platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dataclasses import dataclass

from ..constants import LINUX_AMD64, LINUX_ARM64, MACOSX_AMD64, WINDOWS_AMD64
from ..platform_capabilities import PlatformCapability, PlatformIdentifier
from ..platform_capabilities import CAPABILITY_REGISTRY, PlatformCapability, PlatformIdentifier


@dataclass(frozen=True)
Expand All @@ -25,8 +25,6 @@ def __post_init__(self) -> None:

def get_capability(self) -> PlatformCapability:
"""Get the capability declaration for this platform."""
from ..platform_capabilities import CAPABILITY_REGISTRY

key = f"{self.os_type}-{self.architecture}"
return CAPABILITY_REGISTRY.get(key, self._create_default_capability())

Expand Down
28 changes: 14 additions & 14 deletions solc_select/models/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@
from packaging.version import Version


class SolcVersion(Version):
"""Represents a Solidity compiler version."""

@classmethod
def parse(cls, version_str: str) -> "SolcVersion":
"""Parse a version string into a SolcVersion instance."""
if version_str == "latest":
raise ValueError("Cannot parse 'latest' - resolve to actual version first")
return cls(version_str)


@dataclass(frozen=True)
class VersionRange:
"""Inclusive version range [min, max]. None means unbounded."""

min_version: "SolcVersion | None" = None
max_version: "SolcVersion | None" = None
min_version: SolcVersion | None = None
max_version: SolcVersion | None = None

def contains(self, version: "SolcVersion") -> bool:
def contains(self, version: SolcVersion) -> bool:
"""Check if version is within range (inclusive)."""
above_minimum = self.min_version is None or version >= self.min_version
below_maximum = self.max_version is None or version <= self.max_version
Expand All @@ -30,14 +41,3 @@ def exact_range(cls, min_ver: str, max_ver: str) -> "VersionRange":
min_version=SolcVersion.parse(min_ver),
max_version=SolcVersion.parse(max_ver),
)


class SolcVersion(Version):
"""Represents a Solidity compiler version."""

@classmethod
def parse(cls, version_str: str) -> "SolcVersion":
"""Parse a version string into a SolcVersion instance."""
if version_str == "latest":
raise ValueError("Cannot parse 'latest' - resolve to actual version first")
return cls(version_str)
8 changes: 3 additions & 5 deletions solc_select/repositories.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Repository implementations for fetching Solidity compiler versions."""

from functools import lru_cache
from typing import TYPE_CHECKING, Any
from typing import Any

import requests

Expand All @@ -11,11 +11,9 @@
CRYTIC_SOLC_ARTIFACTS,
CRYTIC_SOLC_JSON,
)
from .models.platforms import Platform
from .models.versions import SolcVersion

if TYPE_CHECKING:
from .models.platforms import Platform


class SolcRepository:
"""Repository for fetching Solidity compiler version information and artifacts."""
Expand Down Expand Up @@ -91,7 +89,7 @@ def get_checksums(self, version: SolcVersion) -> tuple[str, str | None]:
return sha256_hash, keccak256_hash


def SoliditylangRepository(platform: "Platform", session: requests.Session) -> SolcRepository:
def SoliditylangRepository(platform: Platform, session: requests.Session) -> SolcRepository:
"""Create a Soliditylang repository for the given platform."""
platform_key = platform.get_soliditylang_key()
return SolcRepository(
Expand Down
7 changes: 1 addition & 6 deletions solc_select/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
"""
Service layer for solc-select.

This package contains business logic services that orchestrate between
the domain models and infrastructure layers.
"""
"""Service layer for solc-select."""
2 changes: 1 addition & 1 deletion solc_select/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@


def sort_versions(versions: list[str]) -> list[str]:
"""Sorts a list of versions following the component order (major/minor/patch)"""
"""Sort versions by major/minor/patch order."""
return sorted(versions, key=Version)
3 changes: 2 additions & 1 deletion tests/test_network_isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import requests

from solc_select.services.solc_service import SolcService


class TestNetworkIsolation:
"""Test that solc operations don't make unnecessary network requests."""
Expand All @@ -23,7 +25,6 @@ def test_solc_version_no_network_calls_after_install(
The isolated_solc_data fixture ensures the binary path resolution works
correctly by redirecting VIRTUAL_ENV to point to the isolated test directory.
"""
from solc_select.services.solc_service import SolcService

# Phase 1: Install version (network calls expected/allowed)
service = SolcService()
Expand Down