Skip to content
Open
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
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ uv tool install solc-select
pip3 install solc-select
```

To automatically install and use a version, run `solc-select use <version> --always-install`.
To install and use a version in one step, run `solc-select use <version>`. By default,
`use` will download the requested version if it is not already installed. Pass `--offline`
to disable this and require the version to be present locally.

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

Expand All @@ -41,8 +43,8 @@ To automatically install and use a version, run `solc-select use <version> --alw
### Quick Start

```bash
# Install and set a specific Solidity version
solc-select use 0.8.19 --always-install
# Install and set a specific Solidity version (auto-installs if missing)
solc-select use 0.8.19

# Check the current version
solc --version
Expand Down Expand Up @@ -74,14 +76,27 @@ solc, the solidity compiler commandline interface
Version: 0.5.2+commit.1df8f40c.Linux.g++
```

By default, solc-select will halt if you try to use a version that you do not have installed already. Use the `--always-install` flags to bypass this.
By default, `solc-select use` will download the requested version if it is not already
installed:

```shell
$ solc-select use 0.8.1 --always-install
$ solc-select use 0.8.1
Installing '0.8.1'...
Version '0.8.1' installed.
Switched global version to 0.8.1
```

To disable downloading and require the version to already be installed locally, pass
`--offline`:

```shell
$ solc-select use 0.8.1 --offline
Error: Version '0.8.1' is not installed.
```

The legacy `--always-install` flag is accepted for backward compatibility but is now a
no-op (auto-install is the default).

### Available Commands

```shell
Expand All @@ -97,8 +112,11 @@ solc-select use 0.8.19
# List installed versions
solc-select versions

# Install and switch to a version in one command
solc-select use 0.8.19 --always-install
# Install and switch to a version in one command (default behavior)
solc-select use 0.8.19

# Switch only if the version is already installed (no download)
solc-select use 0.8.19 --offline
```

## Getting Help
Expand Down
23 changes: 19 additions & 4 deletions solc_select/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ def solc_select_install(service: SolcService, versions: list[str]) -> None:
sys.exit(0 if success else 1)


def solc_select_use(service: SolcService, version: str, always_install: bool) -> None:
def solc_select_use(service: SolcService, version: str, offline: bool) -> None:
"""Handle the use command."""
service.switch_global_version(version, always_install, silent=False)
service.switch_global_version(version, auto_install=not offline, silent=False)


def solc_select_versions(service: SolcService) -> None:
Expand Down Expand Up @@ -84,7 +84,16 @@ def create_parser() -> argparse.ArgumentParser:
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")
parser_use.add_argument(
"--offline",
action="store_true",
help="do not download missing versions; fail if the requested version is not installed",
)
# Deprecated: kept as a no-op so existing scripts/CI pipelines do not break.
# Auto-install is now the default behavior of `use`.
parser_use.add_argument(
"--always-install", action="store_true", dest="always_install", help=argparse.SUPPRESS
)

parser_versions = subparsers.add_parser(
VERSIONS_COMMAND, help="prints out all installed solc versions"
Expand All @@ -108,7 +117,13 @@ def solc_select() -> None:
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)
if args.always_install:
print(
"Warning: --always-install is deprecated and now a no-op; "
"auto-install is the default. Pass --offline to disable.",
file=sys.stderr,
)
solc_select_use(service, args.version, args.offline)
elif args.command == VERSIONS_COMMAND:
solc_select_versions(service)
elif args.command == UPGRADE_COMMAND:
Expand Down
16 changes: 11 additions & 5 deletions solc_select/services/solc_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,24 @@ def install_versions(self, version_strings: list[str], silent: bool = False) ->
return False

def switch_global_version(
self, version_str: str, always_install: bool = False, silent: bool = False
self, version_str: str, auto_install: bool = True, silent: bool = False
) -> None:
"""Switch to a different global version."""
"""Switch to a different global version.

When ``auto_install`` is True (the default), missing versions are
downloaded and installed before switching. Pass ``auto_install=False``
(e.g. via the ``--offline`` CLI flag) to require that the version is
already installed.
"""
version = self.version_manager.validate_version(version_str)

if self.filesystem.is_installed(version):
self.filesystem.set_global_version(version)
if not silent:
print(f"Switched global version to {version}")
elif always_install:
elif auto_install:
if self.install_versions([str(version)], silent):
self.switch_global_version(str(version), always_install=False, silent=silent)
self.switch_global_version(str(version), auto_install=False, silent=silent)
else:
raise InstallationError(str(version), "Installation failed")
else:
Expand Down Expand Up @@ -144,7 +150,7 @@ def upgrade_architecture(self) -> None:
def execute_solc(self, args: list[str]) -> None:
"""Execute solc with the current version."""
if not self.get_installed_versions():
self.switch_global_version("latest", always_install=True, silent=True)
self.switch_global_version("latest", auto_install=True, silent=True)

try:
version, _ = self.get_current_version()
Expand Down
20 changes: 11 additions & 9 deletions tests/unit/services/test_solc_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def test_switch_already_installed(
else:
assert captured.out == ""

def test_switch_not_installed_no_auto_install(self, solc_service, mock_dependencies):
def test_switch_not_installed_offline(self, solc_service, mock_dependencies):
"""Raises VersionNotInstalledError when version not installed and auto_install=False."""
version = SolcVersion("0.8.19")
available = [SolcVersion("0.8.19"), SolcVersion("0.8.20")]
Expand All @@ -147,13 +147,15 @@ def test_switch_not_installed_no_auto_install(self, solc_service, mock_dependenc
mock_dependencies["version_manager"].get_available_versions.return_value = available

with pytest.raises(VersionNotInstalledError) as exc_info:
solc_service.switch_global_version("0.8.19")
solc_service.switch_global_version("0.8.19", auto_install=False)

assert "0.8.19" in str(exc_info.value)
assert "is not installed" in str(exc_info.value)

def test_switch_not_installed_with_auto_install(self, solc_service, mock_dependencies, capsys):
"""Installs then switches when always_install=True."""
def test_switch_not_installed_auto_install_default(
self, solc_service, mock_dependencies, capsys
):
"""Auto-install is the default: installs then switches without an explicit flag."""
version = SolcVersion("0.8.19")
mock_dependencies["version_manager"].validate_version.return_value = version
# First call: not installed, second call: installed
Expand All @@ -162,7 +164,7 @@ def test_switch_not_installed_with_auto_install(self, solc_service, mock_depende
mock_dependencies["version_manager"].get_available_versions.return_value = [version]
mock_dependencies["artifact_manager"].install_versions.return_value = True

solc_service.switch_global_version("0.8.19", always_install=True)
solc_service.switch_global_version("0.8.19")

# Should call install_versions with the version (silent=False for internal call)
mock_dependencies["version_manager"].resolve_version_strings.assert_called_once_with(
Expand Down Expand Up @@ -195,7 +197,7 @@ def test_switch_invalid_version(self, solc_service, mock_dependencies):
assert "0.8.99" in str(exc_info.value)

def test_switch_installation_fails(self, solc_service, mock_dependencies):
"""Raises InstallationError when installation fails with always_install=True."""
"""Raises InstallationError when auto-install is enabled and installation fails."""
version = SolcVersion("0.8.19")
mock_dependencies["version_manager"].validate_version.return_value = version
mock_dependencies["filesystem"].is_installed.return_value = False
Expand All @@ -204,7 +206,7 @@ def test_switch_installation_fails(self, solc_service, mock_dependencies):
mock_dependencies["artifact_manager"].install_versions.return_value = False

with pytest.raises(InstallationError) as exc_info:
solc_service.switch_global_version("0.8.19", always_install=True)
solc_service.switch_global_version("0.8.19")

assert "0.8.19" in str(exc_info.value)
assert "Installation failed" in str(exc_info.value)
Expand All @@ -224,7 +226,7 @@ def test_switch_not_available_raises_version_not_found(self, solc_service, mock_
mock_dependencies["version_manager"].get_available_versions.return_value = available

with pytest.raises(VersionNotFoundError) as exc_info:
solc_service.switch_global_version("0.4.3")
solc_service.switch_global_version("0.4.3", auto_install=False)

assert "0.4.3" in str(exc_info.value)
# Should show first 5 available versions
Expand Down Expand Up @@ -387,7 +389,7 @@ def test_execute_solc_auto_install_latest(

solc_service.execute_solc(["--version"])

# Should have called switch_global_version with "latest" and always_install=True
# Should have called switch_global_version with "latest" and auto_install=True
mock_dependencies["version_manager"].validate_version.assert_called_with("latest")
mock_subprocess.assert_called_once_with([str(binary_path), "--version"], check=True)

Expand Down
63 changes: 63 additions & 0 deletions tests/unit/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Tests for the CLI entry point in solc_select.__main__."""

from unittest.mock import patch

import pytest

from solc_select.__main__ import solc_select


@pytest.fixture
def patched_service():
"""Patch SolcService so we can assert on switch_global_version calls."""
with patch("solc_select.__main__.SolcService") as service_cls:
yield service_cls.return_value


class TestUseCommand:
"""Tests for the `solc-select use` CLI command."""

def test_use_defaults_to_auto_install(self, patched_service, monkeypatch):
"""`solc-select use 0.8.19` auto-installs by default."""
monkeypatch.setattr("sys.argv", ["solc-select", "use", "0.8.19"])

solc_select()

patched_service.switch_global_version.assert_called_once_with(
"0.8.19", auto_install=True, silent=False
)

def test_use_offline_disables_auto_install(self, patched_service, monkeypatch):
"""`--offline` flips auto_install to False."""
monkeypatch.setattr("sys.argv", ["solc-select", "use", "0.8.19", "--offline"])

solc_select()

patched_service.switch_global_version.assert_called_once_with(
"0.8.19", auto_install=False, silent=False
)

def test_use_always_install_is_deprecated_noop(self, patched_service, monkeypatch, capsys):
"""The legacy `--always-install` flag still parses but warns and is a no-op."""
monkeypatch.setattr("sys.argv", ["solc-select", "use", "0.8.19", "--always-install"])

solc_select()

patched_service.switch_global_version.assert_called_once_with(
"0.8.19", auto_install=True, silent=False
)
captured = capsys.readouterr()
assert "--always-install is deprecated" in captured.err

def test_use_offline_overrides_legacy_always_install(self, patched_service, monkeypatch):
"""`--offline` wins even if the deprecated `--always-install` is also passed."""
monkeypatch.setattr(
"sys.argv",
["solc-select", "use", "0.8.19", "--always-install", "--offline"],
)

solc_select()

patched_service.switch_global_version.assert_called_once_with(
"0.8.19", auto_install=False, silent=False
)