Add Docker, Java, Golang, and other programming languages to runtime image (#8026)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Robert Brennan
2025-05-14 13:46:06 -04:00
committed by GitHub
parent b1dca48c8e
commit 6145552841
4 changed files with 113 additions and 12 deletions

View File

@@ -37,17 +37,18 @@ jobs:
shell: bash
id: define-base-images
run: |
# Only build nikolaik on PRs, otherwise build both nikolaik and ubuntu.
# Only build nikolaik on PRs, otherwise build both nikolaik, nikolaik-full, ubuntu, and ubuntu-full.
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik", include_languages: false },
{ image: "ubuntu:24.04", tag: "ubuntu", include_languages: false }
]')
else
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik", include_languages: false },
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik-full", include_languages: true },
{ image: "ubuntu:24.04", tag: "ubuntu", include_languages: false },
{ image: "ubuntu:24.04", tag: "ubuntu-full", include_languages: true }
]')
fi
echo "base_image=$json" >> "$GITHUB_OUTPUT"
@@ -149,7 +150,7 @@ jobs:
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
- name: Create source distribution and Dockerfile
run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild ${{ matrix.base_image.include_languages && '--include_programming_languages' || '' }}
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV

View File

@@ -32,13 +32,15 @@ def _generate_dockerfile(
base_image: str,
build_from: BuildFromImageType = BuildFromImageType.SCRATCH,
extra_deps: str | None = None,
include_programming_languages: bool = False,
) -> str:
"""Generate the Dockerfile content for the runtime image based on the base image.
Parameters:
- base_image (str): The base image provided for the runtime image
- build_from (BuildFromImageType): The build method for the runtime image.
- extra_deps (str):
- extra_deps (str): Extra dependencies to install
- include_programming_languages (bool): Whether to include programming language dependencies
Returns:
- str: The resulting Dockerfile content
@@ -55,6 +57,7 @@ def _generate_dockerfile(
build_from_scratch=build_from == BuildFromImageType.SCRATCH,
build_from_versioned=build_from == BuildFromImageType.VERSIONED,
extra_deps=extra_deps if extra_deps is not None else '',
include_programming_languages=include_programming_languages,
)
return dockerfile_content
@@ -111,6 +114,7 @@ def build_runtime_image(
dry_run: bool = False,
force_rebuild: bool = False,
extra_build_args: list[str] | None = None,
include_programming_languages: bool = False,
) -> str:
"""Prepares the final docker build folder.
@@ -120,11 +124,12 @@ def build_runtime_image(
- base_image (str): The name of the base Docker image to use
- runtime_builder (RuntimeBuilder): The runtime builder to use
- platform (str): The target platform for the build (e.g. linux/amd64, linux/arm64)
- extra_deps (str):
- extra_deps (str): Extra dependencies to install
- build_folder (str): The directory to use for the build. If not provided a temporary directory will be used
- dry_run (bool): if True, it will only ready the build folder. It will not actually build the Docker image
- force_rebuild (bool): if True, it will create the Dockerfile which uses the base_image
- extra_build_args (List[str]): Additional build arguments to pass to the builder
- include_programming_languages (bool): Whether to include programming language dependencies
Returns:
- str: <image_repo>:<MD5 hash>. Where MD5 hash is the hash of the docker build folder
@@ -142,6 +147,7 @@ def build_runtime_image(
force_rebuild=force_rebuild,
platform=platform,
extra_build_args=extra_build_args,
include_programming_languages=include_programming_languages,
)
return result
@@ -154,6 +160,7 @@ def build_runtime_image(
force_rebuild=force_rebuild,
platform=platform,
extra_build_args=extra_build_args,
include_programming_languages=include_programming_languages,
)
return result
@@ -167,6 +174,7 @@ def build_runtime_image_in_folder(
force_rebuild: bool,
platform: str | None = None,
extra_build_args: list[str] | None = None,
include_programming_languages: bool = False,
) -> str:
runtime_image_repo, _ = get_runtime_image_repo_and_tag(base_image)
lock_tag = f'oh_v{oh_version}_{get_hash_for_lock_files(base_image)}'
@@ -188,6 +196,7 @@ def build_runtime_image_in_folder(
base_image,
build_from=BuildFromImageType.SCRATCH,
extra_deps=extra_deps,
include_programming_languages=include_programming_languages,
)
if not dry_run:
_build_sandbox_image(
@@ -226,7 +235,13 @@ def build_runtime_image_in_folder(
else:
logger.debug(f'Build [{hash_image_name}] from scratch')
prep_build_folder(build_folder, base_image, build_from, extra_deps)
prep_build_folder(
build_folder,
base_image,
build_from,
extra_deps,
include_programming_languages=include_programming_languages,
)
if not dry_run:
_build_sandbox_image(
build_folder,
@@ -251,6 +266,7 @@ def prep_build_folder(
base_image: str,
build_from: BuildFromImageType,
extra_deps: str | None,
include_programming_languages: bool = False,
) -> None:
# Copy the source code to directory. It will end up in build_folder/code
# If package is not found, build from source code
@@ -282,6 +298,7 @@ def prep_build_folder(
base_image,
build_from=build_from,
extra_deps=extra_deps,
include_programming_languages=include_programming_languages,
)
dockerfile_path = Path(build_folder, 'Dockerfile')
with open(str(dockerfile_path), 'w') as f:
@@ -378,6 +395,12 @@ if __name__ == '__main__':
parser.add_argument('--build_folder', type=str, default=None)
parser.add_argument('--force_rebuild', action='store_true', default=False)
parser.add_argument('--platform', type=str, default=None)
parser.add_argument(
'--include_programming_languages',
action='store_true',
default=False,
help='Include programming language dependencies (Java, Go, PHP, Ruby)',
)
args = parser.parse_args()
if args.build_folder is not None:
@@ -409,6 +432,7 @@ if __name__ == '__main__':
dry_run=True,
force_rebuild=args.force_rebuild,
platform=args.platform,
include_programming_languages=args.include_programming_languages,
)
_runtime_image_repo, runtime_image_source_tag = (

View File

@@ -10,6 +10,13 @@ ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry \
GIT_EDITOR="code --wait" \
OPENVSCODE_SERVER_ROOT=/openhands/.openvscode-server
{% if include_programming_languages %}
ENV GOPATH=/go \
GOROOT=/usr/local/go \
PATH="/usr/local/go/bin:/go/bin:/root/.cargo/bin:${PATH}" \
JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
{% endif %}
{% macro setup_base_system() %}
# Install base system dependencies
@@ -27,10 +34,53 @@ RUN apt-get update && \
TZ=Etc/UTC DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends nodejs python3.12 python-is-python3 python3-pip python3.12-venv && \
corepack enable yarn && \
{% endif -%}
{% endif %}
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Docker
RUN apt-get update && \
apt-get install -y apt-transport-https ca-certificates gnupg lsb-release && \
if grep -q "Debian" /etc/os-release; then \
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null; \
else \
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null; \
fi && \
apt-get update && \
apt-get install -y docker-ce docker-ce-cli containerd.io && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
{% if include_programming_languages %}
# Install Java
RUN apt-get update && \
apt-get install -y openjdk-17-jdk maven gradle && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Golang
RUN curl -OL https://golang.org/dl/go1.21.0.linux-amd64.tar.gz && \
tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz && \
rm go1.21.0.linux-amd64.tar.gz && \
echo "export PATH=$PATH:/usr/local/go/bin" >> /etc/profile && \
echo "export PATH=$PATH:/usr/local/go/bin" >> /etc/bash.bashrc
# Install PHP
RUN apt-get update && \
apt-get install -y php && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Ruby
RUN apt-get update && \
apt-get install -y ruby-full ruby-bundler && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
{% endif %}
{% if 'ubuntu' in base_image %}
RUN ln -s "$(dirname $(which node))/corepack" /usr/local/bin/corepack && \
npm install -g corepack && corepack enable yarn && \
@@ -52,6 +102,11 @@ RUN mkdir -p /openhands && \
mkdir -p /openhands/logs && \
mkdir -p /openhands/poetry
{% if include_programming_languages %}
RUN mkdir -p /go && \
chmod -R 777 /go
{% endif %}
{% endmacro %}
{% macro setup_vscode_server() %}
@@ -111,8 +166,19 @@ RUN /openhands/micromamba/bin/micromamba config set changeps1 False && \
/openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.12
# Install project dependencies in smaller chunks
# Ensure we have all necessary build dependencies for pydantic-core
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
g++ \
python3-dev \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install main dependencies first
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry install --only main --no-interaction --no-root
# Then install runtime dependencies
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry install --only runtime --no-interaction --no-root
# Install playwright and its dependencies

View File

@@ -87,6 +87,7 @@ def test_prep_build_folder(temp_dir):
base_image=DEFAULT_BASE_IMAGE,
build_from=BuildFromImageType.SCRATCH,
extra_deps=None,
include_programming_languages=False,
)
# make sure that the code was copied
@@ -133,6 +134,7 @@ def test_generate_dockerfile_build_from_scratch():
dockerfile_content = _generate_dockerfile(
base_image,
build_from=BuildFromImageType.SCRATCH,
include_programming_languages=False,
)
assert base_image in dockerfile_content
assert 'apt-get update' in dockerfile_content
@@ -153,6 +155,7 @@ def test_generate_dockerfile_build_from_lock():
dockerfile_content = _generate_dockerfile(
base_image,
build_from=BuildFromImageType.LOCK,
include_programming_languages=False,
)
# These commands SHOULD NOT include in the dockerfile if build_from_scratch is False
@@ -171,6 +174,7 @@ def test_generate_dockerfile_build_from_versioned():
dockerfile_content = _generate_dockerfile(
base_image,
build_from=BuildFromImageType.VERSIONED,
include_programming_languages=False,
)
# these commands should not exist when build from versioned
@@ -247,7 +251,11 @@ def test_build_runtime_image_from_scratch():
== f'{get_runtime_image_repo()}:{OH_VERSION}_mock-lock-tag_mock-source-tag'
)
mock_prep_build_folder.assert_called_once_with(
ANY, base_image, BuildFromImageType.SCRATCH, None
ANY,
base_image,
BuildFromImageType.SCRATCH,
None,
include_programming_languages=False,
)
@@ -342,6 +350,7 @@ def test_build_runtime_image_exact_hash_not_exist_and_lock_exist():
f'{get_runtime_image_repo()}:{OH_VERSION}_mock-lock-tag',
BuildFromImageType.LOCK,
None,
include_programming_languages=False,
)
@@ -401,6 +410,7 @@ def test_build_runtime_image_exact_hash_not_exist_and_lock_not_exist_and_version
f'{get_runtime_image_repo()}:{OH_VERSION}_mock-versioned-tag',
BuildFromImageType.VERSIONED,
None,
include_programming_languages=False,
)