diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1c0f13b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: Docker Hub + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - '!v[0-9]+.[0-9]+.[0-9]+[ab][0-9]+' + +env: + IMAGE_NAME: csdms/bmi-example-c-grpc4bmi + +jobs: + + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Get version + id: vars + run: echo "version=${GITHUB_REF:11}" >> $GITHUB_OUTPUT + + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + context: images/conda-base + push: true + tags: ${{ env.IMAGE_NAME }}:latest,${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.version }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..519ec7c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +name: Test + +on: + push: + pull_request: + schedule: + - cron: '07 4 3 * *' # 4:07a on third day of the month + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + + test: + name: Run tests + if: + github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + + runs-on: ${{ matrix.os }} + + defaults: + run: + shell: bash -l {0} + + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Install Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install requirements + run: pip install pytest nbmake -r examples/requirements.txt + + - name: Check example script + working-directory: ${{ github.workspace }}/examples + run: | + python run-model-through-grpc4bmi.py + + - name: Check example notebook + run: | + pytest examples --nbmake --nbmake-timeout=3000 -v diff --git a/README.md b/README.md index c88482f..db3a40f 100644 --- a/README.md +++ b/README.md @@ -2,27 +2,45 @@ Set up a [grpc4bmi](https://grpc4bmi.readthedocs.io) server to run a containerized version -of the [Basic Model Interface](https://bmi.readthedocs.io) +of the [Basic Model Interface](https://bmi.readthedocs.io) (BMI) [C example](https://github.com/csdms/bmi-example-c) through Python. ## Build +There are two options for building this project: + +1. from a base image, [source-base](./images/source-base/), where grpc and its dependent libraries, grpc4bmi, and the BMI C example are all built from source +1. from a base image, [conda-base](./images/conda-base/), where grpc and its dependent libraries are installed through conda-forge, the BMI C example is installed from a separate conda-based Docker image, and grpc4bmi is built from source + +In each case, the grpc4bmi server is exposed through port 55555. + +### source-base + Build this example locally with: ``` -docker build --tag bmi-example-c-grpc4bmi . +docker build --tag bmi-example-c-grpc4bmi images/source-base ``` -The image is built on the [mdpiper/grpc4bmi-154](https://hub.docker.com/r/mdpiper/grpc4bmi-154) base image. +The image is (temporarily) built on the [mdpiper/grpc4bmi](https://hub.docker.com/r/mdpiper/grpc4bmi) base image. The OS is Linux/Ubuntu. The C BMI example, grpc4bmi, and the grpc4bmi server are installed in `/usr/local`. -The server is exposed through port 55555. + +### conda-base + +Build this example locally with: +``` +docker build --tag bmi-example-c-grpc4bmi images/conda-base +``` +The image is built on the [csdms/grpc4bmi](https://hub.docker.com/r/csdms/grpc4bmi) base image, +which is built on the [condaforge/miniforge3](https://hub.docker.com/r/condaforge/miniforge3) base image. +The OS is Linux/Ubuntu. +The C BMI example, grpc4bmi, and the grpc4bmi server are installed in `/opt/conda`. ## Run -Use the grpc4bmi [Docker client](https://grpc4bmi.readthedocs.io/en/latest/container/usage.html#docker) -to access the BMI methods of the containerized model. +Use the grpc4bmi Docker client to access the BMI methods of the containerized model. -Install grpc4bmi with *pip*: +Install with *pip*: ``` pip install grpc4bmi ``` @@ -38,11 +56,28 @@ del m # stop container cleanly ``` If the image isn't found locally, it's pulled from Docker Hub -(e.g., try the `mdpiper/bmi-example-c-grpc4bmi-154` image). +(e.g., try the `csdms/bmi-example-c-grpc4bmi` image). For more in-depth examples of running the *Heat* model through grpc4bmi, see the [examples](./examples) directory. +## Developer notes + +A versioned, multiplatform image built from the *conda-base* image in this repository is hosted on Docker Hub +at [csdms/bmi-example-c-grpc4bmi](https://hub.docker.com/r/csdms/bmi-example-c-grpc4bmi). +This image is automatically built and pushed to Docker Hub +with the [release](./.github/workflows/release.yml) CI workflow. +The workflow is only run when the repository is tagged. +To manually build and push an update, run: +``` +docker buildx build --platform linux/amd64,linux/arm64 -t csdms/bmi-example-c-grpc4bmi:latest --push . +``` +A user can pull this image from Docker Hub with: +``` +docker pull csdms/bmi-example-c-grpc4bmi +``` +optionally with the `latest` tag or with a version tag. + ## Acknowledgment This work is supported by the U.S. National Science Foundation under Award No. [2103878](https://www.nsf.gov/awardsearch/showAward?AWD_ID=2103878), *Frameworks: Collaborative Research: Integrative Cyberinfrastructure for Next-Generation Modeling Science*. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..c70fff2 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,22 @@ +# examples + +Two examples--a [Python script](./run-model-through-grpc4bmi.py) +and a [Jupyter notebook](./run-model-through-grpc4bmi.ipynb)--that demonstrate how to run +the C `Heat` model through the grpc4bmi server built in this project. + +In a virtual environment, +install grpc4bmi and other dependencies: +```sh +pip install -r requirements.txt +``` + +Run the Python example: +```sh +python run-model-through-grpc4bmi.py +``` +It'll produce output in the terminal. + +Start JupyterLab and run the example notebook: +```sh +jupyter lab run-model-through-grpc4bmi.ipynb +``` diff --git a/examples/requirements.txt b/examples/requirements.txt index a881ac7..9885329 100644 --- a/examples/requirements.txt +++ b/examples/requirements.txt @@ -1,2 +1,2 @@ grpc4bmi -numpy +jupyter diff --git a/examples/run-model-through-grpc4bmi.ipynb b/examples/run-model-through-grpc4bmi.ipynb index 78ef133..a0b7a9f 100644 --- a/examples/run-model-through-grpc4bmi.ipynb +++ b/examples/run-model-through-grpc4bmi.ipynb @@ -75,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "DOCKER_IMAGE = \"mdpiper/bmi-example-c-grpc4bmi-154\"\n", + "DOCKER_IMAGE = \"csdms/bmi-example-c-grpc4bmi:latest\"\n", "BMI_PORT = 55555\n", "CONFIG_FILE = pathlib.Path(\"/opt/bmi-example-c\") / \"testing\" / \"config.txt\"" ] diff --git a/examples/run-model-through-grpc4bmi.py b/examples/run-model-through-grpc4bmi.py index 66a7236..879fb80 100644 --- a/examples/run-model-through-grpc4bmi.py +++ b/examples/run-model-through-grpc4bmi.py @@ -2,7 +2,7 @@ # Run the C `Heat` model in Python through [grpc4bmi](https://grpc4bmi.readthedocs.io). # # `Heat` models the diffusion of temperature on a uniform rectangular plate with Dirichlet boundary conditions. -# View the model source code and its BMI at https://github.com/csdms/bmi-example-cxx. +# View the model source code and its BMI at https://github.com/csdms/bmi-example-c. # Start by importing some helper libraries. import os @@ -16,7 +16,7 @@ # * which Docker image to use, # * the port exposed through the image, and # * the location in the image of the configuration file used for the model. -DOCKER_IMAGE = "mdpiper/bmi-example-c-grpc4bmi-154" +DOCKER_IMAGE = "csdms/bmi-example-c-grpc4bmi" BMI_PORT = 55555 CONFIG_FILE = pathlib.Path("/opt/bmi-example-c") / "testing" / "config.txt" diff --git a/images/conda-base/Dockerfile b/images/conda-base/Dockerfile new file mode 100644 index 0000000..6343fa3 --- /dev/null +++ b/images/conda-base/Dockerfile @@ -0,0 +1,17 @@ +# A grpc4bmi server for the bmi-example-c `Heat` model. +FROM csdms/grpc4bmi:0.3.0 + +LABEL org.opencontainers.image.authors="Mark Piper " +LABEL org.opencontainers.image.source="https://github.com/csdms/bmi-example-cxx-grpc4bmi" + +COPY server /opt/heatc-grpc4bmi-server +WORKDIR /opt/heatc-grpc4bmi-server/_build +RUN cmake .. -DCMAKE_INSTALL_PREFIX=${CONDA_DIR} && \ + make && \ + make install && \ + make clean + +WORKDIR /opt + +ENTRYPOINT ["/opt/conda/bin/heatc-grpc4bmi-server"] +EXPOSE 55555 diff --git a/images/conda-base/README.md b/images/conda-base/README.md new file mode 100644 index 0000000..d79a84e --- /dev/null +++ b/images/conda-base/README.md @@ -0,0 +1,4 @@ +# conda-base + +This is the version of *bmi-example-c-grpc4bmi* built on conda-forge packages. +See the top-level [README](../../README.md) for more information on building this image. diff --git a/images/conda-base/server/CMakeLists.txt b/images/conda-base/server/CMakeLists.txt new file mode 100644 index 0000000..d54b543 --- /dev/null +++ b/images/conda-base/server/CMakeLists.txt @@ -0,0 +1,45 @@ +# bmi-example-c-grpc4bmi + +cmake_minimum_required(VERSION 3.16) +project(bmi-example-cxx-grpc4bmi + VERSION 0.1.0 + LANGUAGES CXX +) + +include(GNUInstallDirs) + +find_package(PkgConfig REQUIRED) + +pkg_check_modules(BMIHEATC REQUIRED IMPORTED_TARGET bmiheatc) +list(APPEND CMAKE_MESSAGE_INDENT " ") +message(STATUS "Include directory: ${BMIHEATC_INCLUDE_DIRS}") +message(STATUS "Library path: ${BMIHEATC_LINK_LIBRARIES}") +list(POP_BACK CMAKE_MESSAGE_INDENT) + +pkg_check_modules(PROTOBUF REQUIRED IMPORTED_TARGET protobuf) +list(APPEND CMAKE_MESSAGE_INDENT " ") +message(STATUS "Include directory: ${PROTOBUF_INCLUDE_DIRS}") +message(STATUS "Library path: ${PROTOBUF_LINK_LIBRARIES}") +list(POP_BACK CMAKE_MESSAGE_INDENT) + +pkg_check_modules(GRPC4BMI REQUIRED IMPORTED_TARGET grpc4bmi) +list(APPEND CMAKE_MESSAGE_INDENT " ") +message(STATUS "Include directory: ${GRPC4BMI_INCLUDE_DIRS}") +message(STATUS "Library path: ${GRPC4BMI_LINK_LIBRARIES}") +list(POP_BACK CMAKE_MESSAGE_INDENT) + +set(server_exe heatc-grpc4bmi-server) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -lgrpc++_reflection") + +add_executable(${server_exe} ${server_exe}.cxx) +target_link_libraries(${server_exe} + ${BMIHEATC_LINK_LIBRARIES} + ${PROTOBUF_LINK_LIBRARIES} + ${GRPC4BMI_LINK_LIBRARIES} +) +target_include_directories(${server_exe} PUBLIC ${GRPC4BMI_INCLUDE_DIRS}) + +install( + TARGETS ${server_exe} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/server/heatc-grpc4bmi-server.cxx b/images/conda-base/server/heatc-grpc4bmi-server.cxx similarity index 100% rename from server/heatc-grpc4bmi-server.cxx rename to images/conda-base/server/heatc-grpc4bmi-server.cxx diff --git a/Dockerfile b/images/source-base/Dockerfile similarity index 76% rename from Dockerfile rename to images/source-base/Dockerfile index 47253d2..005fbce 100644 --- a/Dockerfile +++ b/images/source-base/Dockerfile @@ -1,5 +1,5 @@ -# grpc4bmi server for the `HeatC` model from bmi-example-c. -FROM mdpiper/grpc4bmi-154 +# A grpc4bmi server for the bmi-example-c `Heat` model. +FROM mdpiper/grpc4bmi LABEL maintainer="Mark Piper " @@ -7,11 +7,10 @@ RUN git clone --branch v2.0.3 --depth 1 https://github.com/csdms/bmi-example-c / WORKDIR /opt/bmi-example-c/_build RUN cmake .. && \ make && \ - make test && \ + ctest -V && \ make install && \ make clean -# Build the grpc4bmi server for the `heatc` model. COPY server /opt/heatc-grpc4bmi-server WORKDIR /opt/heatc-grpc4bmi-server/_build RUN cmake .. && \ diff --git a/images/source-base/README.md b/images/source-base/README.md new file mode 100644 index 0000000..135733c --- /dev/null +++ b/images/source-base/README.md @@ -0,0 +1,5 @@ +# source-base + +This is the version of *bmi-example-c-grpc4bmi* built from source. +See the top-level [README](../../README.md) for more information +on building this image. diff --git a/server/CMakeLists.txt b/images/source-base/server/CMakeLists.txt similarity index 90% rename from server/CMakeLists.txt rename to images/source-base/server/CMakeLists.txt index ad8da16..a14dc5f 100644 --- a/server/CMakeLists.txt +++ b/images/source-base/server/CMakeLists.txt @@ -31,7 +31,11 @@ list(POP_BACK CMAKE_MESSAGE_INDENT) set(server_exe heatc-grpc4bmi-server) add_executable(${server_exe} ${server_exe}.cxx) -target_link_libraries(${server_exe} ${BMIHEATC_LINK_LIBRARIES} ${PROTOBUF_LINK_LIBRARIES} ${GRPC4BMI_LINK_LIBRARIES}) +target_link_libraries(${server_exe} + ${BMIHEATC_LINK_LIBRARIES} + ${PROTOBUF_LINK_LIBRARIES} + ${GRPC4BMI_LINK_LIBRARIES} +) target_include_directories(${server_exe} PUBLIC ${GRPC4BMI_INCLUDE_DIRS}) install( diff --git a/images/source-base/server/heatc-grpc4bmi-server.cxx b/images/source-base/server/heatc-grpc4bmi-server.cxx new file mode 100644 index 0000000..c8f002b --- /dev/null +++ b/images/source-base/server/heatc-grpc4bmi-server.cxx @@ -0,0 +1,26 @@ +#include + +#include "bmi_heat.h" +#include "bmi_grpc_server.h" + + +int main(int argc, char *argv[]) +{ + printf("BmiHeat C grpc4bmi server\n"); + + Bmi *model = (Bmi *) malloc(sizeof(Bmi)); + + register_bmi_heat(model); + + { + char model_name[BMI_MAX_COMPONENT_NAME]; + + model->get_component_name(model, model_name); + printf("%s\n", model_name); + } + + run_bmi_server(model, argc, argv); + + free(model); + return 0; +}