Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ jobs:
- uses: actions/checkout@v4

- name: Build Examples
uses: SilverbackLtd/build-action@v1
uses: SilverbackLtd/build-action@refactor/use-updated-sdk
with:
push: ${{ github.event_name != 'pull_request' }}
tag: latest
Expand Down
14 changes: 0 additions & 14 deletions ape-config.yaml

This file was deleted.

15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,18 @@ extend_skip = ["version.py"]

[tool.mdformat]
number = true

[[tool.ape.plugins]]
name = "etherscan"
[[tool.ape.plugins]]
name = "tokens"

# Ensure we are configured for fork mode correct for example
[tool.ape.ethereum.mainnet-fork]
default_provider = "foundry"

[tool.ape.tokens]
default = "CoinGecko"
required = [
{name = "CoinGecko", uri = "https://tokens.coingecko.com/uniswap/all.json"},
]
1 change: 1 addition & 0 deletions requirements-bot.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# NOTE: The existance of this file (or `requirements-bot.txt`) skips installing via `pyproject.toml`
77 changes: 58 additions & 19 deletions silverback/_build_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path

import click
import tomlkit
import yaml

IMAGES_FOLDER_NAME = ".silverback-images"
Expand All @@ -10,11 +11,11 @@
def dockerfile_template(
bot_path: Path,
sdk_version: str = "stable",
include_bot_dir: bool = False,
has_requirements_txt: bool = False,
requirements_txt_fname: str | None = None,
has_pyproject_toml: bool = False,
has_ape_config_yaml: bool = False,
contracts_folder: str | None = None,
include_bot_dir: bool = False,
):
dockerfile = [
f"FROM ghcr.io/apeworx/silverback:{sdk_version}",
Expand All @@ -24,20 +25,22 @@ def dockerfile_template(
"USER harambe",
]

if has_requirements_txt or has_pyproject_toml:
dockerfile.append("RUN pip install --upgrade pip")

if has_requirements_txt:
dockerfile.append("COPY requirements.txt .")
dockerfile.append("RUN pip install -r requirements.txt")
if requirements_txt_fname:
dockerfile.append(f"COPY {requirements_txt_fname} requirements.txt")

if has_pyproject_toml:
dockerfile.append("COPY pyproject.toml .")
dockerfile.append("RUN pip install .")

if has_ape_config_yaml:
dockerfile.append("COPY ape-config.yaml .")

if requirements_txt_fname or has_pyproject_toml:
dockerfile.append("RUN pip install --upgrade pip")

# NOTE: Only install project via `pyproject.toml` if `requirements-bot].txt` DNE
install_arg = "-r requirements.txt" if requirements_txt_fname else "."
dockerfile.append(f"RUN pip install {install_arg}")

if has_pyproject_toml or has_ape_config_yaml:
dockerfile.append("RUN ape plugins install -U .")

Expand All @@ -58,11 +61,29 @@ def generate_dockerfiles(path: Path, sdk_version: str = "stable"):
contracts_folder: str | None = "contracts"
if has_ape_config_yaml := (ape_config_path := Path.cwd() / "ape-config.yaml").exists():
contracts_folder = (
yaml.safe_load(ape_config_path.read_text())
yaml.safe_load(ape_config_path.read_text()).get("compiler", {})
# NOTE: Should fall through to this last `.get` and use initial default if config DNE
.get("contracts_folder", contracts_folder)
)

if has_pyproject_toml := (pyproject_path := Path.cwd() / "pyproject.toml").exists():
contracts_folder = (
tomlkit.loads(pyproject_path.read_text())
.get("tool", {})
.get("ape", {})
.get("compiler", {})
# NOTE: Should fall through to this last `.get` and use initial default if config DNE
.get("contracts_folder", contracts_folder)
)

if not (
# NOTE: Use this first so we can avoid using legitimate `requirements.txt`
(Path.cwd() / (requirements_txt_fname := "requirements-bot.txt")).exists()
or (Path.cwd() / (requirements_txt_fname := "requirements.txt")).exists()
):
# NOTE: Doesn't exist so make it not be `requirements.txt`
requirements_txt_fname = None

assert contracts_folder # make mypy happy
if not ((Path.cwd() / contracts_folder)).exists():
contracts_folder = None
Expand All @@ -75,8 +96,8 @@ def generate_dockerfiles(path: Path, sdk_version: str = "stable"):
bot,
include_bot_dir=True,
sdk_version=sdk_version,
has_requirements_txt=(Path.cwd() / "requirements.txt").exists(),
has_pyproject_toml=(Path.cwd() / "pyproject.toml").exists(),
requirements_txt_fname=requirements_txt_fname,
has_pyproject_toml=has_pyproject_toml,
has_ape_config_yaml=has_ape_config_yaml,
contracts_folder=contracts_folder,
)
Expand All @@ -87,20 +108,38 @@ def generate_dockerfiles(path: Path, sdk_version: str = "stable"):
dockerfile_template(
path,
sdk_version=sdk_version,
has_requirements_txt=(Path.cwd() / "requirements.txt").exists(),
has_pyproject_toml=(Path.cwd() / "pyproject.toml").exists(),
requirements_txt_fname=requirements_txt_fname,
has_pyproject_toml=has_pyproject_toml,
has_ape_config_yaml=has_ape_config_yaml,
contracts_folder=contracts_folder,
)
)


def build_docker_images():
for dockerfile in (Path.cwd() / IMAGES_FOLDER_NAME).glob("Dockerfile.*"):
command = f"docker build -f {dockerfile.relative_to(Path.cwd())} ."
click.secho(f"{command}", fg="green")
def build_docker_images(
tag_base: str | None = None,
version: str = "latest",
push: bool = False,
):
built_tags = []
build_root = Path.cwd()
for dockerfile in (build_root / IMAGES_FOLDER_NAME).glob("Dockerfile.*"):
bot_name = dockerfile.suffix.lstrip(".") or "bot"
tag = (
f"{tag_base.lower()}-{bot_name.lower()}:{version}"
if tag_base is not None
else f"{build_root.name.lower()}-{bot_name.lower()}:{version}"
)
command = ["docker", "build", "-f", str(dockerfile.relative_to(build_root)), "-t", tag, "."]

click.secho(" ".join(command), fg="green")
try:
subprocess.run(command, shell=True, check=True)
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
raise click.ClickException(str(e))

built_tags.append(tag)

if push:
for tag in built_tags:
subprocess.run(["docker", "push", tag])
46 changes: 42 additions & 4 deletions silverback/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,48 @@ def run(cli_ctx, account, runner_class, recorder_class, max_exceptions, debug, b


@cli.command(section="Local Commands")
@click.option("--generate", is_flag=True, default=False)
@click.option(
"-g",
"--generate",
is_flag=True,
default=False,
help="Generate Dockerfiles first. Defaults to false.",
)
@click.option(
"-t",
"--tag-base",
default=None,
help=(
"The base to use to tag the image. "
"The bot name (or 'bot') is appended to it, following a '-' separator. "
"Defaults to using the name of the folder you are building from."
),
)
@click.option(
"--version",
default="latest",
metavar="VERSION",
help="Version to use in tag. Defaults to 'latest'.",
)
@click.option(
"--push",
is_flag=True,
default=False,
help="Push image to logged-in registry. Defaults to false.",
)
@click.argument("path", required=False, default=None)
def build(generate, path):
"""Generate Dockerfiles and build bot container images"""
def build(generate, tag_base, version, push, path):
"""
Generate Dockerfiles and build bot container images

When '--tag-base' is used, you can control the base of the tag for the image.
For example, '--tag-base project' with bots 'botA.py' and 'botB.py' (under 'bots/') produces
'-t project-bota:latest' and '-t project-botb:latest' respectively.

For building to push to a specific image registry, use '--tag-base' to correctly tag images.
Using '--tag-base ghcr.io/myorg/myproject' with the previous example
'-t ghcr.io/myorg/project-bota:latest' and '-t ghcr.io/myorg/project-botb:latest' respectively.
"""
from silverback._build_utils import (
IMAGES_FOLDER_NAME,
build_docker_images,
Expand Down Expand Up @@ -166,7 +204,7 @@ def build(generate, path):
"You can run `silverback build --generate` to generate it and build."
)

build_docker_images()
build_docker_images(tag_base=tag_base, version=version, push=push)


@cli.command(cls=ConnectedProviderCommand, section="Local Commands")
Expand Down
34 changes: 34 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from pathlib import Path

import pytest

from silverback._build_utils import dockerfile_template
from silverback.utils import decode_topics_from_string, encode_topics_to_string


Expand All @@ -15,3 +18,34 @@
)
def test_topic_encoding(topics):
assert decode_topics_from_string(encode_topics_to_string(topics)) == topics


@pytest.mark.parametrize(
"build_args",
[
dict(),
dict(sdk_version="latest"),
dict(requirements_txt_fname="requirements.txt"),
dict(requirements_txt_fname="requirements-bot.txt"),
dict(has_pyproject_toml=True),
dict(has_ape_config_yaml=True),
dict(contracts_folder="src"),
dict(include_bot_dir=True),
],
)
def test_dockerfile_generation(build_args):
dockerfile = dockerfile_template(
Path(__file__).parent.parent / "bots" / "example.py", **build_args
)
assert "example.py" in dockerfile
assert build_args.get("sdk_version", "stable") in dockerfile
if requirements_txt_fname := build_args.get("requirements_txt_fname"):
assert requirements_txt_fname in dockerfile
if build_args.get("has_pyproject_toml"):
assert "pyproject.toml" in dockerfile
if build_args.get("has_ape_config_yaml"):
assert "ape-config.yaml" in dockerfile
if contracts_folder := build_args.get("contracts_folder"):
assert contracts_folder in dockerfile
if build_args.get("include_bot_dir"):
assert "bots/" in dockerfile
Loading