diff --git a/.github/workflows/_runner-gap9-tiled.yml b/.github/workflows/_runner-gap9-tiled.yml index d456f9f353..a5c8b3ac98 100644 --- a/.github/workflows/_runner-gap9-tiled.yml +++ b/.github/workflows/_runner-gap9-tiled.yml @@ -45,7 +45,6 @@ jobs: source /app/install/gap9-sdk/.gap9-venv/bin/activate source /app/install/gap9-sdk/configs/gap9_evk_audio.sh || true export GVSOC_INSTALL_DIR=/app/install/gap9-sdk/install/workstation - export GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 cd DeeployTest mkdir -p /app/.ccache export CCACHE_DIR=/app/.ccache diff --git a/.github/workflows/_runner-gap9.yml b/.github/workflows/_runner-gap9.yml index d5d8d8e4c0..e1d6e452a6 100644 --- a/.github/workflows/_runner-gap9.yml +++ b/.github/workflows/_runner-gap9.yml @@ -45,7 +45,6 @@ jobs: source /app/install/gap9-sdk/.gap9-venv/bin/activate source /app/install/gap9-sdk/configs/gap9_evk_audio.sh || true export GVSOC_INSTALL_DIR=/app/install/gap9-sdk/install/workstation - export GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 cd DeeployTest mkdir -p /app/.ccache export CCACHE_DIR=/app/.ccache diff --git a/.github/workflows/ci-platform-gap9-tiled.yml b/.github/workflows/ci-platform-gap9-tiled.yml index 0043f8d3e9..4a3afc717b 100644 --- a/.github/workflows/ci-platform-gap9-tiled.yml +++ b/.github/workflows/ci-platform-gap9-tiled.yml @@ -25,6 +25,7 @@ concurrency: jobs: select-env: + if: github.repository == 'pulp-platform/Deeploy' uses: ./.github/workflows/_select-env.yml with: docker_image_deeploy: ${{ github.event.inputs.docker_image_deeploy || github.repository == 'pulp-platform/Deeploy' && 'ghcr.io/pulp-platform/deeploy-gap9:latest'}} diff --git a/.github/workflows/ci-platform-gap9.yml b/.github/workflows/ci-platform-gap9.yml index 079f13c2a5..e9a59b4927 100644 --- a/.github/workflows/ci-platform-gap9.yml +++ b/.github/workflows/ci-platform-gap9.yml @@ -26,6 +26,7 @@ concurrency: jobs: select-env: + if: github.repository == 'pulp-platform/Deeploy' uses: ./.github/workflows/_select-env.yml with: docker_image_deeploy: ${{ github.event.inputs.docker_image_deeploy || (github.repository == 'pulp-platform/Deeploy' && 'ghcr.io/pulp-platform/deeploy-gap9:latest') }} diff --git a/.github/workflows/docker-build-deeploy-gap9.yml b/.github/workflows/docker-build-deeploy-gap9.yml new file mode 100644 index 0000000000..e3dac03274 --- /dev/null +++ b/.github/workflows/docker-build-deeploy-gap9.yml @@ -0,0 +1,150 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +--- +name: Docker • Build Deeploy GAP9 Container + +"on": + workflow_dispatch: + inputs: + docker_image_deeploy: + description: "Deeploy Image to use" + required: false + default: "ghcr.io/pulp-platform/deeploy:latest" + +jobs: + prepare: + name: Fetch branch name or tag + runs-on: ubuntu-latest + outputs: + docker_tag: ${{ steps.generate_tag.outputs.docker_tag }} + steps: + - uses: actions/checkout@v4 + + - name: Set up environment variables + run: | + echo "BRANCH_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV + echo "TAG_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV + echo "IS_TAG=${GITHUB_REF_TYPE}" >> $GITHUB_ENV + + - name: Set Docker tag + id: generate_tag + run: | + if [[ "${{ env.IS_TAG }}" == "tag" ]]; then + echo "docker_tag=${{ env.TAG_NAME }}" >> $GITHUB_OUTPUT + else + echo "docker_tag=${{ env.BRANCH_NAME }}" >> $GITHUB_OUTPUT + fi + + build-deeploy-gap9: + name: Build Deeploy GAP9 Image + needs: [prepare] + runs-on: ${{ matrix.runner }} + outputs: + digest-amd64: ${{ steps.digest.outputs.digest-amd64 }} + digest-arm64: ${{ steps.digest.outputs.digest-arm64 }} + strategy: + fail-fast: false + matrix: + platform: [amd64, arm64] + include: + - platform: amd64 + runner: ubuntu-latest + - platform: arm64 + runner: ubuntu-22.04-arm + steps: + - uses: actions/checkout@v4 + + - name: Free up disk space + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: true + android: true + dotnet: true + haskell: true + large-packages: true + + - uses: docker/setup-buildx-action@v3 + + - name: GHCR Log-in + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Cache for Docker + id: cache + uses: actions/cache@v4 + with: + path: var-ccache + key: ${{ runner.os }}-${{ matrix.platform }}-build-cache-deeploy-gap9 + + - name: Inject build-cache + uses: reproducible-containers/buildkit-cache-dance@v3.1.0 + with: + cache-map: | + { + "var-ccache": "/ccache" + } + skip-extraction: ${{ steps.cache.outputs.cache-hit }} + + - name: Lower Case Repository Name + run: | + echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV} + env: + OWNER: "${{ github.repository_owner }}" + + - name: Load SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Build and push final Deeploy image + id: build + uses: docker/build-push-action@v6 + with: + platforms: linux/${{ matrix.platform }} + context: . + cache-from: type=gha + cache-to: type=gha,mode=min + file: Container/Dockerfile.deeploy-gap9 + push: true + build-args: | + DEEPLOY_IMAGE=${{ github.event.inputs.docker_image_deeploy }} + ssh: default + outputs: type=image,name=ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9,annotation-index=true,name-canonical=true,push=true + + - name: Extract image digest + id: digest + run: echo "digest-${{ matrix.platform }}=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT + + merge-deeploy-gap9-images: + name: Merge Deeploy GAP9 Images + runs-on: ubuntu-latest + needs: [prepare, build-deeploy-gap9] + steps: + - name: GHCR Log-in + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Lower Case Repository Name + run: | + echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV} + env: + OWNER: "${{ github.repository_owner }}" + + - name: Merge Deeploy GAP9 Images + uses: Noelware/docker-manifest-action@v1 + with: + inputs: | + ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9@${{ needs.build-deeploy-gap9.outputs.digest-amd64 }}, + ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9@${{ needs.build-deeploy-gap9.outputs.digest-arm64 }} + tags: | + ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9:latest, + ghcr.io/${{ env.OWNER_LC }}/deeploy-gap9:${{ needs.prepare.outputs.docker_tag }} + push: true diff --git a/.github/workflows/docker-build-deeploy.yml b/.github/workflows/docker-build-deeploy.yml index a4d0221e1f..ff2b507405 100644 --- a/.github/workflows/docker-build-deeploy.yml +++ b/.github/workflows/docker-build-deeploy.yml @@ -109,7 +109,7 @@ jobs: env: OWNER: "${{ github.repository_owner }}" - - name: Build and push final deploy image + - name: Build and push final Deeploy image id: build uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/infra-generate-ccache-gap9.yml b/.github/workflows/infra-generate-ccache-gap9.yml index b189bfd708..038789ce40 100644 --- a/.github/workflows/infra-generate-ccache-gap9.yml +++ b/.github/workflows/infra-generate-ccache-gap9.yml @@ -18,6 +18,7 @@ name: Infrastructure • Generate CCache GAP9 jobs: generate-ccache-gap9: + if: github.repository == 'pulp-platform/Deeploy' runs-on: ubuntu-latest container: image: ${{ github.event.inputs.docker_image_deeploy || 'ghcr.io/pulp-platform/deeploy-gap9:latest' }} @@ -39,7 +40,6 @@ jobs: source /app/install/gap9-sdk/.gap9-venv/bin/activate source /app/install/gap9-sdk/configs/gap9_evk_audio.sh || true export GVSOC_INSTALL_DIR=/app/install/gap9-sdk/install/workstation - export GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 cd DeeployTest mkdir -p /app/.ccache export CCACHE_DIR=/app/.ccache diff --git a/.gitignore b/.gitignore index dc93328e4a..d9e4faace3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ install/ compile_commands.json toolchain/**/*/ + # Node package.json package-lock.json @@ -52,3 +53,7 @@ DeeployTest/Tests/**/generateTest.py DeeployTest/out.txt CHANGELOG_GEN.md + +# Container Artifacts +.pyusbip/ +.cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f1d58d3265..a8f323faf5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,7 @@ exclude: | | .*TEST_.* | .*TestFiles.* | .*runtime.* + | .*\.patch | .*prebuilt/.* ) @@ -71,3 +72,7 @@ repos: - id: yamllint name: Lint YAML Files stages: [pre-commit, pre-merge-commit, pre-push, manual] + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.12.0-2 + hooks: + - id: shfmt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4e08ba71..a277d2361d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This file contains the changelog for the Deeploy project. The changelog is divid ### List of Pull Requests +- Add GAP9 Container Support [#163](https://github.com/pulp-platform/Deeploy/pull/163) - Extend Codeowners [#164](https://github.com/pulp-platform/Deeploy/pull/164) - Support for MaxPool1D and RQSConv1D for PULPOpen [#146](https://github.com/pulp-platform/Deeploy/pull/146) - Use Pre-Commit in CI [#159](https://github.com/pulp-platform/Deeploy/pull/159) @@ -12,10 +13,14 @@ This file contains the changelog for the Deeploy project. The changelog is divid - Update CLI interface Across Project, Fix Tutorial, and Remove Legacy Test [#157](https://github.com/pulp-platform/Deeploy/pull/157) ### Added +- GAP9 Container Support with ARM64 architecture support +- `zsh` and `oh-my-zsh` plugin installation in containers +- Shell Format pre-commit hook - Add integer MaxPool1D for Generic platform and RQSConv1D support for PULPOpen, with corresponding kernel tests. - Added GAP9 Platform Support: Deployer, Bindings, Templates, Tiler, DMA (L3Dma/MchanDma), target library, CI workflows ### Changed +- Cleaned up Docker flow to use a temporary build folder - Switch CI to use pre-commit for linting - Update `pulp-nnx` and `pulp-nn-mixed` submodules to their latest versions - PULP-NN moved to TargetLibraries third-party folder diff --git a/Container/.zshrc b/Container/.zshrc new file mode 100644 index 0000000000..b520309bcc --- /dev/null +++ b/Container/.zshrc @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +export ZSH="$HOME/.oh-my-zsh" + +# Prompt/theme selection. +ZSH_THEME="cypher" + +# Keep plugin set minimal for faster shell startup. +plugins=(git) + +# Load Oh My Zsh. +source $ZSH/oh-my-zsh.sh + +# Quick edit shortcut for this file. +alias zshconfig="nano ~/.zshrc" + +# Append and share history across sessions, without duplicates. +unsetopt HIST_SAVE_BY_COPY +setopt APPEND_HISTORY +setopt SHARE_HISTORY +setopt HIST_IGNORE_ALL_DUPS + +# Ensure GAP9 SDK 'gap' command is not shadowed by an alias. +unalias gap diff --git a/Container/Dockerfile.deeploy b/Container/Dockerfile.deeploy index ee5adc5e4f..2b717e49ec 100644 --- a/Container/Dockerfile.deeploy +++ b/Container/Dockerfile.deeploy @@ -14,15 +14,17 @@ ARG TARGETPLATFORM ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 ENV PATH="/app/install/bender:${PATH}" +ENV DEEPLOY_INSTALL_DIR=/app/install ENV LLVM_INSTALL_DIR=/app/install/llvm -WORKDIR /app +WORKDIR /app/build # Make sure updates in the repo are reflected in the image COPY toolchain/*.patch toolchain/ COPY Makefile ./ -COPY requirements-dev.txt ./ # Compile emulators # WIESEP: We need to already clean up some space, otherwise the GitHub runners run out of disk space @@ -30,8 +32,8 @@ RUN --mount=type=cache,target=/ccache \ ccache -z && \ make pulp-sdk chimera-sdk qemu mempool banshee xtensor && \ make gvsoc && \ - cp -r /app/toolchain/gvsoc/core/requirements.txt /app/core-requirements.txt && \ - cp -r /app/toolchain/gvsoc/gapy/requirements.txt /app/gapy-requirements.txt && \ + cp -r toolchain/gvsoc/core/requirements.txt /app/core-requirements.txt && \ + cp -r toolchain/gvsoc/gapy/requirements.txt /app/gapy-requirements.txt && \ rm -rf toolchain/pulp-sdk toolchain/qemu toolchain/mempool toolchain/banshee toolchain/xtensor toolchain/xtl toolchain/xsimd toolchain/gvsoc && \ ccache -s @@ -54,13 +56,15 @@ fi # Compile Snitch Runtime ENV CC="gcc" ENV CXX="g++" +ENV PATH=/app/install/bender:${PATH} RUN --mount=type=cache,target=/ccache \ ccache -z && \ make snitch_runtime && \ ccache -s # Remove toolchain to make the container lighter -RUN rm -rf toolchain +WORKDIR /app +RUN rm -rf /app/build ########## Stage 2: Lightweight image with precompiled toolchain and emulators ########## FROM ubuntu:22.04 AS deeploy @@ -68,9 +72,12 @@ FROM ubuntu:22.04 AS deeploy ARG TARGETPLATFORM ARG DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 # Export symbols necessary for Deeploy's build flow ENV CMAKE=/usr/bin/cmake +ENV DEEPLOY_INSTALL_DIR=/app/install ENV PULP_SDK_HOME=/app/install/pulp-sdk ENV SNITCH_HOME=/app/install/snitch_cluster ENV CHIMERA_SDK_HOME=/app/install/chimera-sdk @@ -83,7 +90,7 @@ ENV MEMPOOL_HOME=/app/install/mempool ENV BENDER_INSTALL_DIR=/app/install/bender ENV PATH=/app/install/qemu/bin:/app/install/banshee:/app/install/bender:$PATH -WORKDIR /app +WORKDIR /app/build COPY pyproject.toml ./ @@ -99,7 +106,8 @@ RUN apt-get update && \ python-is-python3 \ python3.10-venv \ python3.10-distutils \ - gcc && \ + gcc \ + zsh && \ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ python get-pip.py && \ rm get-pip.py && \ @@ -118,10 +126,19 @@ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ ./cmake-3.24.0-linux-aarch64.sh --prefix=/usr --skip-license; \ fi +COPY Container/.zshrc ./ +# # Install Oh My ZSH +RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended && \ + cp .zshrc /root/.zshrc + COPY --from=toolchain /app/core-requirements.txt ./core-requirements.txt COPY --from=toolchain /app/gapy-requirements.txt ./gapy-requirements.txt -COPY --from=toolchain /app/requirements-dev.txt ./ +COPY requirements-dev.txt ./ RUN pip install -r requirements-dev.txt -r core-requirements.txt -r gapy-requirements.txt # Copy pre-built toolchains and emulators -COPY --from=toolchain /app/install ./install +COPY --from=toolchain /app/install /app/install + +# Remove unused files and clean up to reduce image size +WORKDIR /app +RUN rm -rf /app/build diff --git a/Container/Dockerfile.deeploy-gap9 b/Container/Dockerfile.deeploy-gap9 new file mode 100644 index 0000000000..21724019b4 --- /dev/null +++ b/Container/Dockerfile.deeploy-gap9 @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2026 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +ARG DEEPLOY_IMAGE=ghcr.io/pulp-platform/deeploy:latest +FROM ${DEEPLOY_IMAGE} AS deeploy + +ARG TARGETPLATFORM +ARG DEBIAN_FRONTEND=noninteractive + +ENV GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 +ENV GAP_SDK_HOME=/app/install/gap9-sdk +ENV GAP9_SDK_INSTALL_DIR=/app/install/gap9-sdk +ENV GAP_RISCV_GCC_INSTALL_DIR=/app/install/gcc/gap9 + +WORKDIR /app/build + +# Install SSH keys to access private repositories +RUN mkdir -p -m 0700 ~/.ssh && \ + ssh-keyscan iis-git.ee.ethz.ch >> ~/.ssh/known_hosts && \ + ssh-keyscan github.com >> ~/.ssh/known_hosts + +COPY toolchain/*.patch toolchain/ +COPY Makefile ./ + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y \ + autoconf \ + automake \ + bison \ + ccache \ + clang-format \ + curl \ + device-tree-compiler \ + doxygen \ + flex \ + g++ \ + gcc \ + gcc-x86-64-linux-gnu \ + gdb-multiarch \ + git-lfs \ + gtkwave \ + ibglib2.0-dev \ + libftdi-dev \ + libftdi1 \ + libpixman-1-dev \ + libsdl2-dev \ + libsdl2-ttf-dev \ + libsndfile1-dev \ + libtool \ + libusb-1.0-0-dev \ + nano \ + ninja-build \ + pkg-config \ + protobuf-compiler \ + python-is-python3 \ + python3 \ + python3-dev \ + python3-pip \ + python3.10-distutils \ + python3.10-venv \ + rsync \ + scons \ + ssh \ + sudo \ + telnet \ + texinfo \ + usbutils \ + wget \ + zsh && \ + rm -rf /var/lib/apt/lists/* + +RUN --mount=type=cache,target=/ccache \ + ccache -z && make gap9-toolchain + +COPY Container/gap9-amd64.patch Container/gap9-arm64.patch ./ +COPY scripts/gap9-build_sdk.sh ./scripts/ +RUN --mount=type=ssh \ + --mount=type=cache,target=/ccache \ + --mount=type=cache,target=/root/.cache/pip \ + ccache -z && \ + make gap9-sdk && \ + ccache -s && \ + rm -rf /app/install/gap9-sdk/build && \ + rm -rf /app/install/gap9-sdk/.git + + +# Remove unused files and clean up to reduce image size +WORKDIR /app +RUN rm -rf /app/build \ No newline at end of file diff --git a/Container/Dockerfile.toolchain b/Container/Dockerfile.toolchain index 3bd03b14da..be36a5edf5 100644 --- a/Container/Dockerfile.toolchain +++ b/Container/Dockerfile.toolchain @@ -10,12 +10,15 @@ ARG TARGETPLATFORM ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 ENV CC="ccache gcc" ENV CXX="ccache g++" ENV CCACHE_DIR=/ccache +ENV DEEPLOY_INSTALL_DIR=/app/install # Change the working directory -WORKDIR /app +WORKDIR /app/build # Install dependencies RUN apt-get update && \ @@ -84,7 +87,5 @@ RUN --mount=type=cache,target=/ccache \ make picolibc-riscv && ccache -s # Remove unecessary files -RUN rm -rf cmake-* && \ - rm -rf toolchain/llvm-project && \ - rm -rf toolchain/minimalloc && \ - rm -rf toolchain/picolibc \ No newline at end of file +WORKDIR /app +RUN rm -rf build diff --git a/Container/Makefile b/Container/Makefile index cd07d8632b..e2ba85a65d 100644 --- a/Container/Makefile +++ b/Container/Makefile @@ -4,39 +4,63 @@ # Variables TOOLCHAIN_IMAGE ?= ghcr.io/pulp-platform/deeploy-toolchain:latest -DEEPOY_IMAGE ?= ghcr.io/pulp-platform/deeploy:latest +DEEPLOY_IMAGE ?= ghcr.io/pulp-platform/deeploy:latest +DEEPLOY_GAP9_IMAGE ?= ghcr.io/pulp-platform/deeploy-gap9:latest -.PHONY: all toolchain deeploy push clean help +SSH_PRIVATE_KEY ?= ~/.ssh/id_ed25519 + +.PHONY: all toolchain deeploy deeploy-gap9 push clean help help: @echo "Makefile for building Deeploy Docker images" @echo "" @echo "Usage:" - @echo " make toolchain # Build only the toolchain image" - @echo " make deeploy # Build only the deploy image" - @echo " make all # Build both images" - @echo " make clean # Remove all images (optional and dangerous)" + @echo " make toolchain # Build only the toolchain image" + @echo " make deeploy # Build only the deeploy image" + @echo " make deeploy-gap9 # Build only the GAP9 deeploy image" + @echo " make all # Build all images" + @echo " make clean # Remove all images (optional and dangerous)" @echo "" @echo "Build Variables:" - @echo " TOOLCHAIN_IMAGE # Name of the toolchain image (default: $(TOOLCHAIN_IMAGE))" - @echo " DEEPOY_IMAGE # Name of the deploy image (default: $(DEEPOY_IMAGE))" + @echo " TOOLCHAIN_IMAGE # Name of the toolchain image (default: $(TOOLCHAIN_IMAGE))" + @echo " DEEPLOY_IMAGE # Name of the deeploy image (default: $(DEEPLOY_IMAGE))" + @echo " DEEPLOY_GAP9_IMAGE # Name of the GAP9 deeploy image (default: $(DEEPLOY_GAP9_IMAGE))" + @echo " SSH_PRIVATE_KEY # Path to SSH private key for GAP9 build (default: $(SSH_PRIVATE_KEY))" @echo "" @echo "Example Usage:" @echo " make toolchain TOOLCHAIN_IMAGE=my-toolchain:latest" - @echo " make deeploy DEEPOY_IMAGE=my-deploy:latest" - @echo " make all TOOLCHAIN_IMAGE=my-toolchain:latest DEEPOY_IMAGE=my-deploy:latest" + @echo " make deeploy DEEPLOY_IMAGE=my-deeploy:latest" + @echo " make deeploy-gap9 DEEPLOY_GAP9_IMAGE=my-deeploy-gap9:latest" + @echo " make all TOOLCHAIN_IMAGE=my-toolchain:latest DEEPLOY_IMAGE=my-deeploy:latest" + @echo " make deeploy-gap9 SSH_PRIVATE_KEY=/path/to/key" # Build only the toolchain image toolchain: - DOCKER_BUILDKIT=1 docker build -f Dockerfile.toolchain -t $(TOOLCHAIN_IMAGE) .. + DOCKER_BUILDKIT=1 docker build \ + -f Dockerfile.toolchain \ + -t $(TOOLCHAIN_IMAGE) .. -# Build the final deploy image using the toolchain image +# Build the final Deeploy image using the toolchain image deeploy: - DOCKER_BUILDKIT=1 docker build -f Dockerfile.deeploy --build-arg BASE_IMAGE=$(TOOLCHAIN_IMAGE) -t $(DEEPOY_IMAGE) .. + DOCKER_BUILDKIT=1 docker build \ + --ssh default \ + -f Dockerfile.deeploy \ + --build-arg BASE_IMAGE=$(TOOLCHAIN_IMAGE) \ + -t $(DEEPLOY_IMAGE) .. + +# Build the GAP9 Deeploy image +deeploy-gap9: + eval $$(ssh-agent) && \ + ssh-add $(SSH_PRIVATE_KEY) && \ + DOCKER_BUILDKIT=1 docker build \ + --ssh default \ + -f Dockerfile.deeploy-gap9 \ + --build-arg DEEPLOY_IMAGE=$(DEEPLOY_IMAGE) \ + -t $(DEEPLOY_GAP9_IMAGE) .. -# Build both -all: toolchain deeploy +# Build all images +all: toolchain deeploy deeploy-gap9 # Clean all images (optional and dangerous) clean: - docker rmi $(TOOLCHAIN_IMAGE) $(DEEPOY_IMAGE) || true + docker rmi $(TOOLCHAIN_IMAGE) $(DEEPLOY_IMAGE) $(DEEPLOY_GAP9_IMAGE) || true diff --git a/Container/gap9-amd64.patch b/Container/gap9-amd64.patch new file mode 100644 index 0000000000..df9cd1c50f --- /dev/null +++ b/Container/gap9-amd64.patch @@ -0,0 +1,32 @@ +diff --git a/.gitignore b/.gitignore +index 2d63047dd..221d67f8b 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -56,3 +56,5 @@ gaptest_reports/ + # Release script dry-run output + release_dry_run/ + artifact.txt ++ ++.gap9-venv +diff --git a/merged_requirements.txt b/merged_requirements.txt +index bf711ddbf..ec08bfe0b 100644 +--- a/merged_requirements.txt ++++ b/merged_requirements.txt +@@ -2,7 +2,7 @@ Cython>=0.29.21 + Mako>=1.1.3 + Ninja + Pillow>=7.2.0 +-PyQt5 ++# PyQt5 + PyYAML>=6.0.1 + QGraphViz + Sphinx<7.2.0,>=3.0.0 +@@ -48,7 +48,7 @@ ptyprocess>=0.7.0 + pybind11>=2.6 + pycryptodome + pyelftools>=0.29 +-pyqt5 ++# pyqt5 + pyserial>=3.5 + pytablewriter>=1.2.0 + python-dateutil>=2.7 diff --git a/Container/gap9-arm64.patch b/Container/gap9-arm64.patch new file mode 100644 index 0000000000..16f94dfe18 --- /dev/null +++ b/Container/gap9-arm64.patch @@ -0,0 +1,55 @@ +diff --git a/.gitignore b/.gitignore +index 2d63047dd..221d67f8b 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -56,3 +56,5 @@ gaptest_reports/ + # Release script dry-run output + release_dry_run/ + artifact.txt ++ ++.gap9-venv +diff --git a/Makefile b/Makefile +index ff1ca8d6a..9b30be6fe 100644 +--- a/Makefile ++++ b/Makefile +@@ -118,7 +118,7 @@ openocd.checkout: + fi + + openocd.build: openocd.checkout +- cd utils/openocd && ./bootstrap && ./configure --enable-jtag_dpi --disable-werror --prefix=$(INSTALL_DIR)/openocd && $(MAKE) && $(MAKE) install ++ cd utils/openocd && ./bootstrap && ./configure --enable-jtag_dpi --enable-ftdi --disable-werror --prefix=$(INSTALL_DIR)/openocd --build=aarch64-unknown-linux-gnu && $(MAKE) && $(MAKE) install + + + openocd.checkout.gap9.5: +diff --git a/configs/common.sh b/configs/common.sh +index ccb7d5f0c..2606972bf 100644 +--- a/configs/common.sh ++++ b/configs/common.sh +@@ -294,3 +294,5 @@ if [ -n "$GAP_SDK_CI_BUILD" -a -z "$GAP_SDK_CI_DISABLE_GITHUB_ENV" -a -n "$GITHU + fi + done + fi ++ ++export WITH_EMPTY_SFU=1 +diff --git a/merged_requirements.txt b/merged_requirements.txt +index bf711ddbf..ec08bfe0b 100644 +--- a/merged_requirements.txt ++++ b/merged_requirements.txt +@@ -2,7 +2,7 @@ Cython>=0.29.21 + Mako>=1.1.3 + Ninja + Pillow>=7.2.0 +-PyQt5 ++# PyQt5 + PyYAML>=6.0.1 + QGraphViz + Sphinx<7.2.0,>=3.0.0 +@@ -48,7 +48,7 @@ ptyprocess>=0.7.0 + pybind11>=2.6 + pycryptodome + pyelftools>=0.29 +-pyqt5 ++# pyqt5 + pyserial>=3.5 + pytablewriter>=1.2.0 + python-dateutil>=2.7 diff --git a/DeeployTest/Platforms/GAP9/CMakeLists.txt b/DeeployTest/Platforms/GAP9/CMakeLists.txt index 0a7fde9c00..db06a4e38f 100644 --- a/DeeployTest/Platforms/GAP9/CMakeLists.txt +++ b/DeeployTest/Platforms/GAP9/CMakeLists.txt @@ -29,4 +29,7 @@ target_compile_options(network PRIVATE -Wno-error ) +target_link_options(${ProjectId} PRIVATE + -Wl,--print-memory-usage +) link_compile_dump(${TESTNAME}) diff --git a/Makefile b/Makefile index d40a49da11..f007f105c1 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,9 @@ PICOLIBC_RV32IMA_INSTALL_DIR ?= ${LLVM_INSTALL_DIR}/picolibc/riscv/rv32ima PICOLIBC_RV32IMAFD_INSTALL_DIR ?= ${LLVM_INSTALL_DIR}/picolibc/riscv/rv32imafd PICOLIBC_RV32IMF_INSTALL_DIR ?= ${LLVM_INSTALL_DIR}/picolibc/riscv/rv32imf +GCC_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/gcc +GAP_RISCV_GCC_INSTALL_DIR ?= ${GCC_INSTALL_DIR}/gap9 + CHIMERA_SDK_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/chimera-sdk PULP_SDK_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/pulp-sdk SNITCH_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/snitch_cluster @@ -36,6 +39,7 @@ MINIMALLOC_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/minimalloc XTL_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/xtl XSIMD_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/xsimd XTENSOR_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/xtensor +GAP9_SDK_INSTALL_DIR ?= ${DEEPLOY_INSTALL_DIR}/gap9-sdk CMAKE ?= cmake @@ -52,19 +56,28 @@ XTL_VERSION ?= 0.7.5 XSIMD_VERSION ?= 13.2.0 XTENSOR_VERSION ?= 0.25.0 +GAP_RISCV_GCC_COMMIT_HASH ?= fbb9fa450d01c1c170f94af817490f41c5ef7971 +# GAP9_SDK_COMMIT_HASH ?= 1796873cec9ca1feb352a6fe980b627df979bdd1 # v5.21.1 +# GAP9_SDK_COMMIT_HASH ?= 8c42b65338e554ac73c749f94ecddd23a9ee5490 # v5.21.1-staging-1 +GAP9_SDK_COMMIT_HASH ?= d075c068e8a531e74a9f7cdee74c52cec32253b9 # v5.21.2 +GAP_SDK_URL ?= git@github.com:pulp-platform/gap-sdk.git + OS := $(shell uname -s) ARCH:= $(shell uname -m) ifeq ($(OS),Linux) ifeq ($(ARCH),x86_64) - TARGET := x86_64-unknown-linux-gnu + TARGET_BANSHEE := x86_64-unknown-linux-gnu + TARGET_GAP9 := amd64 else ifeq ($(ARCH),aarch64) - TARGET := aarch64-unknown-linux-gnu + TARGET_BANSHEE := aarch64-unknown-linux-gnu + TARGET_GAP9 := arm64 else $(error unsupported Linux architecture $(ARCH)) endif else ifeq ($(OS),Darwin) - TARGET := aarch64-apple-darwin + TARGET_BANSHEE := aarch64-apple-darwin + $(warning "Deeploy is not fully supported on macOS, some components such as the GAP9 GCC toolchain are not available. Please use Linux (or Docker) for the best experience.") else $(error unsupported platform $(OS)) endif @@ -77,6 +90,8 @@ echo-bash: @echo "The following symbols need to be exported for Deeploy to work properly:" @echo "export MINIMALLOC_INSTALL_DIR=${MINIMALLOC_INSTALL_DIR}" @echo "export PULP_SDK_HOME=${PULP_SDK_INSTALL_DIR}" + @echo "export GAP_SDK_HOME=${GAP9_SDK_INSTALL_DIR}" + @echo "export GAP_RISCV_GCC_TOOLCHAIN=${GAP_RISCV_GCC_INSTALL_DIR}" @echo "export CHIMERA_SDK_HOME=${CHIMERA_SDK_INSTALL_DIR}" @echo "export SNITCH_HOME=${SNITCH_INSTALL_DIR}" @echo "export GVSOC_INSTALL_DIR=${GVSOC_INSTALL_DIR}" @@ -97,9 +112,11 @@ emulators: snitch_runtime pulp-sdk qemu banshee mempool ${TOOLCHAIN_DIR}/llvm-project: cd ${TOOLCHAIN_DIR} && \ - git clone https://github.com/pulp-platform/llvm-project.git \ - -b main && \ - cd ${TOOLCHAIN_DIR}/llvm-project && git checkout ${LLVM_COMMIT_HASH} && \ + git init llvm-project && \ + cd ${TOOLCHAIN_DIR}/llvm-project && \ + git remote add origin https://github.com/pulp-platform/llvm-project.git && \ + git fetch --depth=1 origin ${LLVM_COMMIT_HASH} && \ + git checkout ${LLVM_COMMIT_HASH} && \ git submodule update --init --recursive ${LLVM_INSTALL_DIR}: ${TOOLCHAIN_DIR}/llvm-project @@ -410,6 +427,23 @@ ${PULP_SDK_INSTALL_DIR}: ${TOOLCHAIN_DIR}/pulp-sdk pulp-sdk: ${PULP_SDK_INSTALL_DIR} +${GAP_RISCV_GCC_INSTALL_DIR}: + mkdir -p ${GAP_RISCV_GCC_INSTALL_DIR} && cd ${GAP_RISCV_GCC_INSTALL_DIR} && \ + curl -LO https://github.com/pulp-platform/gap-riscv-gnu-toolchain/releases/download/v0.0.1/gap9-gcc-ubuntu22.04-$(TARGET_GAP9).tar.gz && \ + tar -xzf gap9-gcc-ubuntu22.04-$(TARGET_GAP9).tar.gz --strip-components=1 -C . && \ + rm gap9-gcc-ubuntu22.04-$(TARGET_GAP9).tar.gz + +gap9-toolchain: ${GAP_RISCV_GCC_INSTALL_DIR} + +.PHONY: gap9-sdk +gap9-sdk: + @echo "Cloning and building GAP9 SDK..." + GAP9_SDK_INSTALL_DIR=${GAP9_SDK_INSTALL_DIR} \ + GAP9_SDK_COMMIT_HASH=${GAP9_SDK_COMMIT_HASH} \ + GAP_SDK_URL=${GAP_SDK_URL} \ + ROOT_DIR=${ROOT_DIR} \ + bash ${ROOT_DIR}/scripts/gap9-build_sdk.sh + ${TOOLCHAIN_DIR}/snitch_cluster: cd ${TOOLCHAIN_DIR} && \ git clone https://github.com/pulp-platform/snitch_cluster.git && \ @@ -510,18 +544,12 @@ ${QEMU_INSTALL_DIR}: ${TOOLCHAIN_DIR}/qemu qemu: ${QEMU_INSTALL_DIR} -${TOOLCHAIN_DIR}/banshee: - cd ${TOOLCHAIN_DIR} && \ - git clone https://github.com/pulp-platform/banshee.git && \ - cd ${TOOLCHAIN_DIR}/banshee && git checkout ${BANSHEE_COMMIT_HASH} && \ - git submodule update --init --recursive && \ - git apply ${TOOLCHAIN_DIR}/banshee.patch - ${BANSHEE_INSTALL_DIR}: export LLVM_SYS_150_PREFIX=${LLVM_INSTALL_DIR} && \ mkdir -p ${BANSHEE_INSTALL_DIR} && cd ${BANSHEE_INSTALL_DIR} && \ - curl -LO https://github.com/pulp-platform/banshee/releases/download/v0.5.0-prebuilt/banshee-0.5.0-$(TARGET).tar.gz && \ - tar -xzf banshee-0.5.0-$(TARGET).tar.gz --strip-components=1 -C . + curl -LO https://github.com/pulp-platform/banshee/releases/download/v0.5.0-prebuilt/banshee-0.5.0-$(TARGET_BANSHEE).tar.gz && \ + tar -xzf banshee-0.5.0-$(TARGET_BANSHEE).tar.gz --strip-components=1 -C . && \ + rm banshee-0.5.0-$(TARGET_BANSHEE).tar.gz banshee: ${BANSHEE_INSTALL_DIR} diff --git a/README_GAP9.md b/README_GAP9.md new file mode 100644 index 0000000000..d649fbb6e9 --- /dev/null +++ b/README_GAP9.md @@ -0,0 +1,90 @@ +## Using Deeploy with GAP9 + +> ⚠️ **IMPORTANT NOTE** +> This is a work in progress. The GAP9 support in Deeploy is experimental and may not be fully functional. + +To use Deeploy with GAP9, a custom Docker container is required because the official Deeploy Docker image does not yet include the necessary SDKs and dependencies for GAP9 development, because they are not publicly available. + +### Build The Docker Container + +To use SSH keys for accessing private repositories during the Docker build process, make sure you have an SSH key pair set up on your local machine. By default, the Makefile uses the key located at `~/.ssh/id_ed25519`. If your key is located elsewhere, you can specify its path using the `SSH_PRIVATE_KEY` variable when invoking the make command. + +To build a local version of the Deeploy Docker image with GAP9 support using the upstream toolchain image, run: +```sh +cd Container + +# Build the Deeploy image with the upstream toolchain image +make deeploy-gap9 DEEPLOY_GAP9_IMAGE=deeploy-gap9:latest + +# If you want to specify a custom SSH key path, use: +make deeploy-gap9 DEEPLOY_GAP9_IMAGE=deeploy-gap9:latest SSH_PRIVATE_KEY=/path/to/your/private/key +``` + +Or, to build the toolchain, Deeploy and GAP9 images locally, use: +```sh +cd Container + +# To build the Deeploy container with the local toolchain image +make deeploy TOOLCHAIN_IMAGE=deeploy-toolchain:gap9 DEEPLOY_IMAGE=deeploy:gap9 + +# To build the Deeploy GAP9 container with the local toolchain image +make deeploy-gap9 TOOLCHAIN_IMAGE=deeploy-toolchain:gap9 DEEPLOY_IMAGE=deeploy:gap9 DEEPLOY_GAP9_IMAGE=deeploy-gap9:latest +``` + +### Use The Docker Container + +Once the image is built, you can create and start the container in interactive mode with: + +```sh +docker run -it --name deeploy_gap9 -v $(pwd):/app/Deeploy deeploy-gap9:latest +``` + +Before running tests, you need to set up the GAP9 environment inside the container: +```sh +source /app/install/gap9-sdk/.gap9-venv/bin/activate +source /app/install/gap9-sdk/configs/gap9_evk_audio.sh +export GVSOC_INSTALL_DIR=/app/install/gap9-sdk/install/workstation +``` +Install Deeploy inside the container in editable mode: + +```sh +cd /app/Deeploy +pip install -e . +``` + +```sh +cd /app/Deeploy/DeeployTest +python deeployRunner_gap9.py -t ./Tests/Kernels/FP32/MatMul +python deeployRunner_tiled_gap9.py -t ./Tests/Kernels/FP32/MatMul +``` + +### Use A Real GAP9 Board (USB/IP via gap9-run.sh) + +For board access, use the orchestration script in [scripts/gap9-run.sh](scripts/gap9-run.sh). It manages: +- host-side USB/IP server (pyusbip) +- usbip device manager container +- GAP9 SDK container with the correct mounts + +#### Prerequisites +- Docker installed and running +- A working SSH key for BuildKit (if you are building the image locally) +- USB/IP host support (the script can set up pyusbip on the host) + + +#### Start the board workflow (recommended) +This launches a tmux session with three panes: one for the host USB/IP server, one for the GAP9 container and one terminal on the host to manage the USB/IP devices. + +```sh +./scripts/gap9-run.sh start-tmux +``` + +#### Start manually (two terminals) +Terminal 1 (host USB/IP server): +```sh +./scripts/gap9-run.sh start-usbip-host +``` + +Terminal 2 (containers + attach device): +```sh +./scripts/gap9-run.sh start +``` \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 6ca3d33c6f..88b86d601c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -70,6 +70,7 @@ ["main", "https://pulp-platform.github.io/Deeploy"], ["devel", "https://pulp-platform.github.io/Deeploy/branch/devel"], ["v0.2.0", "https://pulp-platform.github.io/Deeploy/tag/v0.2.0"], + ["v0.2.1", "https://pulp-platform.github.io/Deeploy/tag/v0.2.1"], ], } diff --git a/scripts/gap9-build_sdk.sh b/scripts/gap9-build_sdk.sh new file mode 100755 index 0000000000..227169573a --- /dev/null +++ b/scripts/gap9-build_sdk.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2026 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +# gap9-build_sdk.sh +# Helper script to clone, patch and build the GAP9 SDK. Intended to be +# invoked from the Makefile with environment variables set: +# GAP9_SDK_INSTALL_DIR (required) +# GAP9_SDK_COMMIT_HASH (optional, fallback provided) +# ROOT_DIR (optional, defaults to script dir) + +ROOT_DIR="${ROOT_DIR:-$(cd "$(dirname "$0")" && pwd)}" +GAP9_SDK_INSTALL_DIR="${GAP9_SDK_INSTALL_DIR:?GAP9_SDK_INSTALL_DIR must be set}" +GAP9_SDK_COMMIT_HASH="${GAP9_SDK_COMMIT_HASH:-897955d7ab326bd31684429eb16a2e485ab89afb}" +GAP_SDK_URL="${GAP_SDK_URL:-git@iis-git.ee.ethz.ch:wiesep/gap9_sdk.git}" + +echo "Preparing GAP9 SDK in: ${GAP9_SDK_INSTALL_DIR}" + +if [ -d "${GAP9_SDK_INSTALL_DIR}/.git" ]; then + echo "Directory ${GAP9_SDK_INSTALL_DIR} already exists and looks like a git repo. Updating remote URL and fetching latest changes..." + cd "${GAP9_SDK_INSTALL_DIR}" || exit 1 + git remote set-url origin "${GAP_SDK_URL}" || true +else + echo "Cloning GAP9 SDK..." + GIT_LFS_SKIP_SMUDGE=1 git clone "${GAP_SDK_URL}" "${GAP9_SDK_INSTALL_DIR}" +fi + +cd "${GAP9_SDK_INSTALL_DIR}" || exit 1 +echo "Checking out commit ${GAP9_SDK_COMMIT_HASH} (stash and fetch if necessary)" +git fetch --all --tags || true +git stash || true +GIT_LFS_SKIP_SMUDGE=1 git checkout "${GAP9_SDK_COMMIT_HASH}" +git submodule update --init --recursive + +# Platform specific patch +ARCH=$(dpkg --print-architecture 2>/dev/null || true) +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi +case "$ARCH" in +amd64 | x86_64) PATCH=gap9-amd64.patch ;; +arm64 | aarch64) PATCH=gap9-arm64.patch ;; +*) PATCH= ;; +esac + +set -e # Enable strict error handling for the build process +if [ -n "$PATCH" ] && [ -f "${ROOT_DIR}/${PATCH}" ]; then + echo "Applying platform patch: $PATCH" + git apply "${ROOT_DIR}/${PATCH}" +else + echo "No platform-specific patch to apply for architecture '$ARCH' (looked for ${ROOT_DIR}/${PATCH})" +fi +set +e # Disable strict error handling to allow deactivation even if build fails + +echo "Setting up Python virtual environment and installing dependencies" +python -m venv .gap9-venv +. .gap9-venv/bin/activate +pip install "numpy<2.0.0" +echo "Sourcing GAP9 SDK environment" +. configs/gap9_evk_audio.sh || true + +echo "Invoking make install_dependency cmake_sdk.build" +set -e # Enable strict error handling for the build process +make install_dependency cmake_sdk.build openocd.all autotiler.all +set +e # Disable strict error handling to allow deactivation even if build fails + +deactivate + +echo "GAP9 SDK ready at: ${GAP9_SDK_INSTALL_DIR}" + +exit 0 diff --git a/scripts/gap9-run.sh b/scripts/gap9-run.sh new file mode 100755 index 0000000000..af2dc20e0a --- /dev/null +++ b/scripts/gap9-run.sh @@ -0,0 +1,540 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2026 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +# gap9-run.sh - Docker orchestration script for GAP9 SDK with USB/IP device passthrough +# +# +# This script manages the containerized GAP9 development environment, including: +# - Building the GAP9 Docker image +# - Running the usbip device manager container (for USB device passthrough) +# - Starting the GAP9 SDK container with mounted volumes +# +# Prerequisites: +# - Docker installed and running +# - SSH key configured for BuildKit (if building image) +# - pyusbip server running on host (for USB passthrough) +# + +set -euo pipefail + +######################################################################### +# Configuration & Defaults +######################################################################### + +# Image and container names +GAP9_IMAGE="ghcr.io/pulp-platform/deeploy-gap9" +USBIP_IMAGE="jonathanberi/devmgr" + +DOCKER_PLATFORM="auto" +DOCKER_SHELL="/bin/zsh" + +# USB/IP device settings +USBIP_HOST="host.docker.internal" +USBIP_VENDOR="0403" +USBIP_PRODUCT="6011" + +# SDK and cache directories +WORK_DIR="." +CACHE_FOLDER=".cache" + +# SSH key gap9 container +SSH_PRIVATE_KEY="~/.ssh/id_ed25519" + +# pyusbip configuration +PYUSBIP_REPO="https://github.com/tumayt/pyusbip" +PYUSBIP_DIR=".pyusbip" + +######################################################################### +# Utility Functions +######################################################################### + +# Print colored output +log_info() { + echo -e "\033[0;36m[INFO ]\033[0m $*" +} + +log_error() { + echo -e "\033[0;31m[ERROR ]\033[0m $*" >&2 +} + +log_warn() { + echo -e "\033[0;33m[WARN ]\033[0m $*" >&2 +} + +log_success() { + echo -e "\033[0;32m[SUCCESS]\033[0m $*" +} + +# Display help message +show_help() { + cat < [options] + +GAP9 Docker Orchestration Script + +Commands: + start Start usbip daemon and GAP9 container + start-tmux Start everything in a tmux session with split panes + stop Stop containers + start-usbip-host Setup and run host-side USB/IP server (in separate terminal) + start-gap9 Start only the GAP9 container + start-usbip-daemon Start only the usbip device manager container + attach-usbip Attach USB device to usbip daemon + detach-usbip Detach USB device from usbip daemon + setup-usbip-host One-time setup for host-side USB/IP server + help Display this help message + +Options: + -i, --image NAME Docker image name (default: $GAP9_IMAGE) + -d, --work-dir PATH Path to working directory (default: $WORK_DIR) + -c, --cache-dir PATH Cache directory (default: $CACHE_FOLDER) + -k, --ssh-key PATH SSH private key (default: $SSH_PRIVATE_KEY) + -h, --host ADDR usbip host address (default: $USBIP_HOST) + -v, --vendor ID USB vendor ID (default: $USBIP_VENDOR) + -p, --product ID USB product ID (default: $USBIP_PRODUCT) + --platform PLATFORM Docker platform (default: $DOCKER_PLATFORM) + --shell SHELL Shell to use in container (default: $DOCKER_SHELL) + +Examples: + # Start everything in tmux (recommended) + $0 start-tmux + + # Start containers with USB device passthrough (manual terminals) + $0 start-usbip-host # In terminal 1 + $0 start # In terminal 2 + + # Custom working directory + $0 -d /path/to/workdir start + + # Stop everything + $0 stop + +EOF +} + +# Parse command-line arguments (collect options first, then run command) +command="" +opts=() +args=("$@") +idx=0 +while [[ $idx -lt ${#args[@]} ]]; do + case "${args[$idx]}" in + -i | --image) + GAP9_IMAGE="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -d | --work-dir) + WORK_DIR="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -c | --cache-dir) + CACHE_FOLDER="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -k | --ssh-key) + SSH_PRIVATE_KEY="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -h | --host) + USBIP_HOST="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -v | --vendor) + USBIP_VENDOR="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + -p | --product) + USBIP_PRODUCT="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + --platform) + DOCKER_PLATFORM="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + --shell) + DOCKER_SHELL="${args[$((idx + 1))]}" + opts+=("${args[$idx]}" "${args[$((idx + 1))]}") + idx=$((idx + 2)) + ;; + help | --help) + show_help + exit 0 + ;; + start | start-tmux | stop | start-gap9 | start-usbip-daemon | attach-usbip | detach-usbip | stop-usbip-daemon | start-usbip-host | setup-usbip-host) + if [[ -n "$command" ]]; then + log_error "Multiple commands provided: $command and ${args[$idx]}" + show_help + exit 1 + fi + command="${args[$idx]}" + idx=$((idx + 1)) + ;; + *) + log_error "Unknown option or command: ${args[$idx]}" + show_help + exit 1 + ;; + esac +done + +# Expand path of SSH private key +SSH_PRIVATE_KEY="$(eval echo "$SSH_PRIVATE_KEY")" + +## Print configuration +log_info "Configuration:" +log_info " GAP9 Docker Image: $GAP9_IMAGE" +log_info " Working Directory: $WORK_DIR" +log_info " Cache Directory: $CACHE_FOLDER" +log_info " SSH Private Key: $SSH_PRIVATE_KEY" +log_info " USB/IP Host: $USBIP_HOST" +log_info " USB Vendor ID: $USBIP_VENDOR" +log_info " USB Product ID: $USBIP_PRODUCT" +log_info " Docker Platform: $DOCKER_PLATFORM" +log_info " Docker Shell: $DOCKER_SHELL" + +######################################################################### +# usbip Host Setup Functions +######################################################################### + +cmd_setup_usbip_host() { + log_info "Setting up host-side USB/IP server..." + + # Clone pyusbip if not present + if [ ! -d "$PYUSBIP_DIR" ]; then + log_info "Cloning pyusbip into $PYUSBIP_DIR..." + git clone "$PYUSBIP_REPO" "$PYUSBIP_DIR" + else + log_info "pyusbip directory $PYUSBIP_DIR already exists, skipping clone" + fi + + # Create virtual environment if needed + if [ ! -d "$PYUSBIP_DIR/.venv" ]; then + log_info "Creating Python virtual environment..." + python3 -m venv "$PYUSBIP_DIR/.venv" + + log_info "Installing pyusbip dependencies..." + . "$PYUSBIP_DIR/.venv/bin/activate" + pip install --upgrade pip + pip install libusb1 + else + log_info "Virtual environment already exists, skipping setup" + fi + + log_success "Host-side USB/IP setup complete" +} + +cmd_start_usbip_host() { + cmd_setup_usbip_host + + log_info "Starting host-side USB/IP server (pyusbip)..." + log_info "This process will run in the foreground. Press Ctrl+C to stop." + + cd "$PYUSBIP_DIR" && + . .venv/bin/activate && + python pyusbip.py +} + +######################################################################### +# usbip Daemon Functions +######################################################################### + +# Wait for usbip server to be ready +wait_for_usbip_server() { + local max_retries=20 + local retry_count=0 + local retry_interval=1 + + log_info "Waiting for pyusbip server to be ready..." + + while [ $retry_count -lt $max_retries ]; do + if pgrep -f "python.*pyusbip\.py" >/dev/null; then + log_success "pyusbip server is ready" + return 0 + fi + + retry_count=$((retry_count + 1)) + log_info " Attempt $retry_count/$max_retries: pyusbip not ready yet, retrying in ${retry_interval}s..." + sleep "$retry_interval" + done + + log_error "Timeout waiting for pyusbip server to start (${max_retries}s)" + return 1 +} + +cmd_start_usbip_daemon() { + # Wait for pyusbip server to be ready + if ! wait_for_usbip_server; then + log_error "pyusbip server did not start in time" + log_error "Please run '$0 start-usbip-host' in a separate terminal first" + exit 1 + fi + + # Check if container already running + if [ -n "$(docker ps -q -f name=usbip-devmgr)" ]; then + log_info "usbip-devmgr container already running" + return 0 + fi + + log_info "Starting usbip-devmgr container..." + docker run -d --rm \ + --privileged \ + --pid=host \ + --name usbip-devmgr \ + -e USBIP_HOST="$USBIP_HOST" \ + -e USBIP_VENDOR="$USBIP_VENDOR" \ + -e USBIP_PRODUCT="$USBIP_PRODUCT" \ + "$USBIP_IMAGE" \ + /bin/sh -lc 'nsenter -t1 -m sh -lc "tail -f /dev/null"' + + log_success "usbip-devmgr container started" +} + +cmd_attach_usbip() { + # First detach any existing attachment + cmd_detach_usbip || true + + log_info "Attaching USB device to usbip-devmgr..." + docker exec -it usbip-devmgr /bin/sh -lc 'nsenter -t1 -m sh -lc " + usbip list -r \"$USBIP_HOST\" || { echo \"usbip list failed\"; exit 1; } + BUSID=\$(usbip list -r \"$USBIP_HOST\" \ + | grep \"$USBIP_VENDOR:$USBIP_PRODUCT\" \ + | head -n1 \ + | cut -d\":\" -f1 \ + | xargs) + if [ -z \"\$BUSID\" ]; then + exit 1 + fi + usbip attach -r \"$USBIP_HOST\" -b \"\$BUSID\" + "' + + if [ $? -ne 0 ]; then + log_error "Failed to attach USB device" + else + log_success "USB device attached successfully" + fi +} + +cmd_detach_usbip() { + log_info "Detaching USB device..." + docker run --rm \ + --privileged \ + --pid=host \ + -e USBIP_HOST="$USBIP_HOST" \ + -e USBIP_VENDOR="$USBIP_VENDOR" \ + -e USBIP_PRODUCT="$USBIP_PRODUCT" \ + "$USBIP_IMAGE" \ + /bin/sh -lc 'nsenter -t1 -m sh -lc " + PORT=\$(usbip port \ + | grep \"$USBIP_VENDOR:$USBIP_PRODUCT\" -B 1 \ + | head -n1 \ + | sed -E \"s/^Port ([0-9]+):.*/\1/\" \ + | xargs) + if [ -z \"\$PORT\" ]; then + exit 1 + fi + usbip detach -p \"\$PORT\" + "' >/dev/null 2>&1 + + if [ $? -ne 0 ]; then + log_warn "Failed to detach USB device (it may not have been attached)" + else + log_success "USB device detached (or not attached)" + fi +} + +cmd_stop_usbip_daemon() { + log_info "Stopping usbip-devmgr container..." + + # First detach the device + cmd_detach_usbip || true + + # Stop the container + if [ -n "$(docker ps -q -f name=usbip-devmgr)" ]; then + docker stop usbip-devmgr >/dev/null 2>&1 + log_success "usbip-devmgr container stopped" + else + log_info "usbip-devmgr container not running" + fi +} + +######################################################################### +# GAP9 Container Functions +######################################################################### + +cmd_start_gap9() { + log_info "Starting GAP9 container..." + + # Prepare cache directory + mkdir -p "$CACHE_FOLDER" + touch "$CACHE_FOLDER/.zsh_history" + + # Validate WORK_DIR exists + if [ ! -d "$WORK_DIR" ]; then + log_error "WORK_DIR not found: $WORK_DIR" + log_error "Use -d/--work-dir to set the SDK path" + exit 1 + fi + + log_info "Press Ctrl+D or type 'exit' to exit container" + + # Build docker run command with optional platform argument + local docker_run_args=( + -it --rm + --privileged + -v /dev/bus/usb:/dev/bus/usb + -v "$SSH_PRIVATE_KEY":/root/.ssh/id_ed25519:ro + -v "$WORK_DIR/":/app/work/ + -v "$CACHE_FOLDER/.zsh_history":/root/.zsh_history + -v "$CACHE_FOLDER/ccache":/ccache + -e CCACHE_DIR=/ccache + ) + + # Add platform argument if not 'auto' + if [[ "$DOCKER_PLATFORM" != "auto" ]]; then + docker_run_args+=(--platform "$DOCKER_PLATFORM") + fi + + docker_run_args+=("$GAP9_IMAGE" "$DOCKER_SHELL" -c "cd /app/work && $DOCKER_SHELL") + + docker run "${docker_run_args[@]}" +} + +######################################################################### +# Orchestration Functions +######################################################################### + +cmd_start() { + log_info "Starting GAP9 orchestration (usbip daemon + GAP9 container)..." + cmd_start_usbip_daemon + cmd_attach_usbip + cmd_start_gap9 +} + +cmd_stop() { + log_info "Stopping all containers..." + cmd_stop_usbip_daemon + cmd_stop_tmux + log_success "All containers stopped" +} + +######################################################################### +# Tmux Orchestration +######################################################################### + +cmd_start_tmux() { + local session_name="gap9-dev" + local script_path="$0" + local opts_escaped="" + + for opt in ${opts[@]+"${opts[@]}"}; do + if [[ -n "$opt" ]]; then + printf -v opt '%q' "$opt" + opts_escaped+=" $opt" + fi + done + + # Check if tmux is installed + if ! command -v tmux &>/dev/null; then + log_error "tmux is not installed. Please install tmux first." + log_error "On macOS: brew install tmux" + log_error "On Linux: sudo apt-get install tmux" + exit 1 + fi + + # Kill any existing session with the same name + tmux kill-session -t "$session_name" 2>/dev/null || true + + log_info "Creating tmux session: $session_name" + + # Create new session with three panes (usbip-host, usbip-daemon, gap9) + tmux new-session -d -s "$session_name" -x 200 -y 50 + + # First pane: run pyusbip server + + # Second pane: run main orchestration (with delay to let server start) + tmux split-window -t "$session_name:0" -h + tmux split-window -t "$session_name:0" -v + tmux send-keys -t "$session_name:0.1" "alias stop='$script_path$opts_escaped stop'" Enter + tmux send-keys -t "$session_name:0.1" "$script_path$opts_escaped start-usbip-host" Enter + tmux send-keys -t "$session_name:0.2" "alias stop='$script_path$opts_escaped stop'" Enter + tmux send-keys -t "$session_name:0.2" "$script_path$opts_escaped start-usbip-daemon" Enter + tmux send-keys -t "$session_name:0.2" "$script_path$opts_escaped attach-usbip" Enter + tmux send-keys -t "$session_name:0.0" "alias stop='$script_path$opts_escaped stop'" Enter + tmux send-keys -t "$session_name:0.0" "$script_path$opts_escaped start-gap9" Enter + + # Select the first pane + tmux select-pane -t "$session_name:0.0" + + log_success "tmux session created: $session_name" + log_info "Attaching to session..." + log_info "To detach: Ctrl+B then D" + log_info "To kill session: tmux kill-session -t $session_name" + + # Attach to the session + tmux attach-session -t "$session_name" +} + +cmd_stop_tmux() { + local session_name="gap9-dev" + log_info "Stopping tmux session: $session_name" + tmux kill-session -t "$session_name" 2>/dev/null || log_info "tmux session $session_name not running" +} + +######################################################################### +# Main Script Logic +######################################################################### + +if [[ -z "$command" ]]; then + cmd_start_tmux + exit 0 +fi + +# Execute the command after options are parsed +case "$command" in +start) + cmd_start + ;; +start-tmux) + cmd_start_tmux + ;; +stop) + cmd_stop + ;; +start-gap9) + cmd_start_gap9 + ;; +start-usbip-daemon) + cmd_start_usbip_daemon + ;; +attach-usbip) + cmd_attach_usbip + ;; +detach-usbip) + cmd_detach_usbip + ;; +stop-usbip-daemon) + cmd_stop_usbip_daemon + ;; +start-usbip-host) + cmd_start_usbip_host + ;; +setup-usbip-host) + cmd_setup_usbip_host + ;; +*) + log_error "Unknown command: $command" + show_help + exit 1 + ;; +esac