From ab815dfeec4e9eec006451890f61904e9afb9aae Mon Sep 17 00:00:00 2001 From: Ziv Yaniv Date: Tue, 19 May 2026 11:42:57 -0400 Subject: [PATCH 1/6] Binary distributions via GitHub Releases. For all triggering events, the workflow builds SimpleITK using the installer, creates binary packages from the results and uploads the artifacts to GitHub. When the triggering event is a tag push that matches the SITK_TARGET (DESCRIPTION file) a draft release is created and the packages are uploaded to it. --- .github/workflows/main.yml | 100 ++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 07ae00b..4d0ac50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,8 @@ on: push: branches: - main + tags: + - 'v*' # Trigger on version tags pull_request: branches: - main @@ -28,6 +30,7 @@ jobs: with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} + - name: Setup R uses: r-lib/actions/setup-r@v2 with: @@ -55,7 +58,7 @@ jobs: - name: Install R packages shell: bash run: | - R -e "install.packages(c('remotes'), lib=Sys.getenv('R_LIBS'), repos='https://cloud.r-project.org/')" + R -e "install.packages(c('remotes', 'pkgbuild'), lib=Sys.getenv('R_LIBS'), repos='https://cloud.r-project.org/')" - name: Build and test shell: bash env: @@ -63,3 +66,98 @@ jobs: run: | R -e "Sys.setenv(MAKEJ=2); remotes::install_git(c('.'), lib=Sys.getenv('R_LIBS'))" R -e "library(SimpleITK); Version()" + - name: Create binary package from installed package + id: create_package + shell: bash + run: | + # Get the R version for naming + R_VERSION="${{ matrix.R }}" + R_VERSION_SHORT=$(echo $R_VERSION | cut -d'.' -f1,2) + # Create output directory for artifacts + mkdir -p artifacts + # Build binary package from the installed package (no recompilation needed) + # The configure script already built everything and remotes::install_git installed it + R -e "pkg <- file.path(Sys.getenv('R_LIBS'), 'SimpleITK'); pkgbuild::build(pkg, dest_path='artifacts', binary=TRUE)" + # Rename with descriptive platform info + if [[ "$RUNNER_OS" == "macOS" ]]; then + PKG_FILE=$(ls artifacts/SimpleITK_*.tgz) + NEW_NAME="${PKG_FILE%.tgz}_R${R_VERSION_SHORT}_macos-x86_64.tgz" + elif [[ "$RUNNER_OS" == "Linux" ]]; then + PKG_FILE=$(ls artifacts/SimpleITK_*.tar.gz) + NEW_NAME="${PKG_FILE%.tar.gz}_R${R_VERSION_SHORT}_linux-x86_64.tar.gz" + elif [[ "$RUNNER_OS" == "Windows" ]]; then + PKG_FILE=$(ls artifacts/SimpleITK_*.zip) + NEW_NAME="${PKG_FILE%.zip}_R${R_VERSION_SHORT}_windows-x86_64.zip" + fi + mv "$PKG_FILE" "$NEW_NAME" + ls -lh artifacts/ + + # Export PKG_NAME as output for use in upload step + echo "pkg_name=${PKG_NAME}" >> $GITHUB_OUTPUT + - name: Upload binary package + if: steps.create_package.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.create_package.outputs.pkg_name }} + path: artifacts/* + retention-days: 30 + + create-release: + name: Create GitHub Draft Release + # Only run this job for tag pushes after the R-build job completes + # successfully. + if: startsWith(github.ref, 'refs/tags/v') + needs: R-build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Verify tag matches SITK_TARGET + id: verify_tag + run: | + SITK_TARGET=$(grep '^SITK_TARGET:' DESCRIPTION | awk '{print $2}') + TAG_NAME="${{ github.ref_name }}" + if [ "${TAG_NAME}" != "${SITK_TARGET}" ]; then + echo "Tag ${TAG_NAME} does not match SITK_TARGET ${SITK_TARGET}" + echo "Skipping draft release creation." + echo "draft_release=false" >> $GITHUB_OUTPUT + else + echo "draft_release=true" >> $GITHUB_OUTPUT + fi + + - name: Download all artifacts + if: steps.verify_tag.outputs.draft_release == 'true' + uses: actions/download-artifact@v4 + with: + path: release-artifacts + pattern: SimpleITK_* + merge-multiple: true + + - name: Display downloaded artifacts + if: steps.verify_tag.outputs.draft_release == 'true' + run: | + echo "Downloaded artifacts:" + ls -lhR release-artifacts/ + + # This action automatically creates the release if it doesn't exist, + # or updates it if it does. + - name: Create or Update Draft Release + if: steps.verify_tag.outputs.draft_release == 'true' + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + draft: true + name: SimpleITK ${{ github.ref_name }} R Package Release + body: | + Please review and test the packages before publishing this release. + Then remove the "Draft" status to make it public and delete this message. + files: | + release-artifacts/* + fail_on_unmatched_files: true From ac19b80ab47e016be38d838f82a94666162403e3 Mon Sep 17 00:00:00 2001 From: Ziv Yaniv Date: Thu, 21 May 2026 12:13:40 -0400 Subject: [PATCH 2/6] Security improvements to workflow. Refer to actions using the hash and not the tag that can be moved. Also limit permissions to read-only. --- .github/workflows/main.yml | 55 +++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d0ac50..3a7e41e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,11 @@ on: # run testing on the first of each month 5am ET / 9am UTC - cron: '0 9 1 * *' +# Set minimal permissions for all jobs (read-only) +permissions: + contents: read + actions: read + jobs: R-build: strategy: @@ -22,17 +27,15 @@ jobs: os: [ 'macos-15-intel', 'ubuntu-latest', 'windows-latest'] runs-on: ${{ matrix.os }} name: ${{ matrix.R }} ${{ matrix.os }} build - env: - R_LIBS: ${{ github.workspace }}/Rlibs steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - - name: Setup R - uses: r-lib/actions/setup-r@v2 + - name: Setup R (also sets the R_LIBS_USER environment variable) + uses: r-lib/actions/setup-r@a51a8012b0aab7c32ef9d19bf54da93f3254335e # v2.12.0 with: r-version: ${{ matrix.R }} - name: System Dependencies @@ -44,7 +47,6 @@ jobs: - name: Configuration Information shell: bash run: | - mkdir -p "$R_LIBS" cmake --version if [[ "$RUNNER_OS" == "Windows" ]]; then ls -d /c/rtools* 2>/dev/null || echo "No rtools found in /c/" @@ -58,45 +60,48 @@ jobs: - name: Install R packages shell: bash run: | - R -e "install.packages(c('remotes', 'pkgbuild'), lib=Sys.getenv('R_LIBS'), repos='https://cloud.r-project.org/')" + R -e "install.packages(c('remotes'), lib=Sys.getenv('R_LIBS_USER'), repos='https://cloud.r-project.org/')" - name: Build and test shell: bash env: ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS: 2 run: | - R -e "Sys.setenv(MAKEJ=2); remotes::install_git(c('.'), lib=Sys.getenv('R_LIBS'))" + R -e "Sys.setenv(MAKEJ=2); remotes::install_git(c('.'), lib=Sys.getenv('R_LIBS_USER'))" R -e "library(SimpleITK); Version()" - name: Create binary package from installed package id: create_package shell: bash run: | # Get the R version for naming - R_VERSION="${{ matrix.R }}" - R_VERSION_SHORT=$(echo $R_VERSION | cut -d'.' -f1,2) + R_VERSION_SHORT=$(echo "${{ matrix.R }}" | cut -d'.' -f1,2) + + # Get package version from DESCRIPTION + PKG_VERSION=$(Rscript -e "cat(read.dcf(file.path(Sys.getenv('R_LIBS_USER'), 'SimpleITK', 'DESCRIPTION'), 'Version')[1])") + # Create output directory for artifacts mkdir -p artifacts - # Build binary package from the installed package (no recompilation needed) - # The configure script already built everything and remotes::install_git installed it - R -e "pkg <- file.path(Sys.getenv('R_LIBS'), 'SimpleITK'); pkgbuild::build(pkg, dest_path='artifacts', binary=TRUE)" - # Rename with descriptive platform info + + # Create binary package archive from installed package (no recompilation) + cd "${R_LIBS_USER}" if [[ "$RUNNER_OS" == "macOS" ]]; then - PKG_FILE=$(ls artifacts/SimpleITK_*.tgz) - NEW_NAME="${PKG_FILE%.tgz}_R${R_VERSION_SHORT}_macos-x86_64.tgz" + PKG_NAME="SimpleITK_${PKG_VERSION}_R${R_VERSION_SHORT}_macos-x86_64.tgz" + tar czf "${GITHUB_WORKSPACE}/artifacts/${PKG_NAME}" SimpleITK elif [[ "$RUNNER_OS" == "Linux" ]]; then - PKG_FILE=$(ls artifacts/SimpleITK_*.tar.gz) - NEW_NAME="${PKG_FILE%.tar.gz}_R${R_VERSION_SHORT}_linux-x86_64.tar.gz" + PKG_NAME="SimpleITK_${PKG_VERSION}_R${R_VERSION_SHORT}_linux-x86_64.tar.gz" + tar czf "${GITHUB_WORKSPACE}/artifacts/${PKG_NAME}" SimpleITK elif [[ "$RUNNER_OS" == "Windows" ]]; then - PKG_FILE=$(ls artifacts/SimpleITK_*.zip) - NEW_NAME="${PKG_FILE%.zip}_R${R_VERSION_SHORT}_windows-x86_64.zip" + PKG_NAME="SimpleITK_${PKG_VERSION}_R${R_VERSION_SHORT}_windows-x86_64.zip" + 7z a -tzip "${GITHUB_WORKSPACE}/artifacts/${PKG_NAME}" SimpleITK fi - mv "$PKG_FILE" "$NEW_NAME" + + cd "${GITHUB_WORKSPACE}" ls -lh artifacts/ # Export PKG_NAME as output for use in upload step echo "pkg_name=${PKG_NAME}" >> $GITHUB_OUTPUT - name: Upload binary package if: steps.create_package.outcome == 'success' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.create_package.outputs.pkg_name }} path: artifacts/* @@ -114,7 +119,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 @@ -133,7 +138,7 @@ jobs: - name: Download all artifacts if: steps.verify_tag.outputs.draft_release == 'true' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: release-artifacts pattern: SimpleITK_* @@ -149,7 +154,7 @@ jobs: # or updates it if it does. - name: Create or Update Draft Release if: steps.verify_tag.outputs.draft_release == 'true' - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 1c370c38acd9cedc355c9dd2662ce001987a408f Mon Sep 17 00:00:00 2001 From: Ziv Yaniv Date: Tue, 26 May 2026 20:58:28 -0400 Subject: [PATCH 3/6] Improve binary packaging. Stripped debug symbols from the binaries to reduce package size. Hides symbols by default in shared libraries and hide inline C++ functions to reduce binary size and prevent symbol conflicts with other packages. --- configure | 5 +++++ configure.win | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/configure b/configure index 9caa5ee..7afdfb9 100755 --- a/configure +++ b/configure @@ -54,9 +54,14 @@ mkdir -p SITK mkdir -p Build && cd Build && cmake \ + -D "CMAKE_CXX_FLAGS:STRING=-fvisibility=hidden -fvisibility-inlines-hidden ${CFLAGS}" \ + -D "CMAKE_C_FLAGS:STRING=-fvisibility=hidden ${CFLAGS}" \ + -DITK_C_OPTIMIZATION_FLAGS:STRING="" \ + -DITK_CXX_OPTIMIZATION_FLAGS:STRING="" \ -DWRAP_DEFAULT=OFF\ -DWRAP_R=ON \ -DSimpleITK_BUILD_DISTRIBUTE=ON \ + -DSimpleITK_BUILD_STRIP:BOOL=ON \ -DBUILD_EXAMPLES=OFF \ -DBUILD_TESTING=OFF \ -DCMAKE_BUILD_TYPE=MinSizeRel \ diff --git a/configure.win b/configure.win index d2fd253..7cc5e69 100755 --- a/configure.win +++ b/configure.win @@ -83,9 +83,14 @@ export MAKEJ # Configure with Unix Makefiles (Rtools toolchain) cmake -G "Unix Makefiles" \ + -D "CMAKE_CXX_FLAGS:STRING=-fvisibility=hidden -fvisibility-inlines-hidden ${CFLAGS}" \ + -D "CMAKE_C_FLAGS:STRING=-fvisibility=hidden ${CFLAGS}" \ + -DITK_C_OPTIMIZATION_FLAGS:STRING="" \ + -DITK_CXX_OPTIMIZATION_FLAGS:STRING="" \ -DWRAP_DEFAULT=OFF \ -DWRAP_R=ON \ -DSimpleITK_BUILD_DISTRIBUTE=ON \ + -DSimpleITK_BUILD_STRIP:BOOL=ON \ -DBUILD_EXAMPLES=OFF \ -DBUILD_TESTING=OFF \ -DITK_SKIP_PATH_LENGTH_CHECK=ON \ From 415f9926bbad5b61b4589bd0d8173af74b463985 Mon Sep 17 00:00:00 2001 From: Ziv Yaniv Date: Wed, 27 May 2026 15:27:26 -0400 Subject: [PATCH 4/6] Use formal language in the DESCRIPTION file. --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 2198dcd..18ab584 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -13,7 +13,7 @@ Authors@R: c(person("Richard", "Beare", role = c("aut", "cre"), email = "blowekamp@mail.nih.gov"), person("Ziv", "Yaniv", role="aut", email = "zivyaniv@nih.gov")) -Author: Richard Beare, Bradley Lowekamp, Ziv Yaniv plus loads of others +Author: Richard Beare, Bradley Lowekamp, Ziv Yaniv, The Insight Software Consortium and the ITK user and developer communities. Depends: R (>= 4.0) Imports: methods, desc From e27322bd1f3bdec4edb9ba7b7699d8830acb3d78 Mon Sep 17 00:00:00 2001 From: Ziv Yaniv Date: Wed, 27 May 2026 17:33:40 -0400 Subject: [PATCH 5/6] Create a CRAN-like binary distribution repository. Use a gh-pages branch for GitHub pages hosting of a CRAN-like repository. When a release is created, there is a workflow that automatically updates the CRAN-like repository using the information from the release assets. Note that the CRAN-like repository does not include the binary packages themselves, it forwards to the URLs of the release assets on GitHub. --- .github/workflows/main.yml | 87 +++++++--- .github/workflows/update_gh_cran.yml | 59 +++++++ update_cran_repo.R | 246 +++++++++++++++++++++++++++ 3 files changed, 372 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/update_gh_cran.yml create mode 100644 update_cran_repo.R diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3a7e41e..3deb401 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: R: [ '4.4.3', '4.5.3', '4.6.0' ] - os: [ 'macos-15-intel', 'ubuntu-latest', 'windows-latest'] + os: [ 'macos-15-intel', 'ubuntu-24.04', 'windows-latest'] runs-on: ${{ matrix.os }} name: ${{ matrix.R }} ${{ matrix.os }} build @@ -66,44 +66,56 @@ jobs: env: ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS: 2 run: | - R -e "Sys.setenv(MAKEJ=2); remotes::install_git(c('.'), lib=Sys.getenv('R_LIBS_USER'))" + R -e "Sys.setenv(MAKEJ=2); remotes::install_git(c('.'), lib=Sys.getenv('R_LIBS_USER'), upgrade='never', INSTALL_opts='--build')" R -e "library(SimpleITK); Version()" - - name: Create binary package from installed package - id: create_package + - name: Rename package artifact + # The original artifact name is SimpleITK_.zip (windows), + # SimpleITK_.tgz (macOS), or SimpleITK_.tar.gz (Linux). + # This is irrespective of the R version. We need to rename them so that they are + # unique. Otherwise we could not upload artifacts for different + # R versions because the names collide. + id: rename_package shell: bash run: | - # Get the R version for naming - R_VERSION_SHORT=$(echo "${{ matrix.R }}" | cut -d'.' -f1,2) + # canonical approach to collecting build artifacts in a directory for upload + mkdir -p artifacts - # Get package version from DESCRIPTION - PKG_VERSION=$(Rscript -e "cat(read.dcf(file.path(Sys.getenv('R_LIBS_USER'), 'SimpleITK', 'DESCRIPTION'), 'Version')[1])") + # remotes::install_git(..., INSTALL_opts='--build') writes a valid package + # file into the current working directory (GITHUB_WORKSPACE). + PKG_PATH=$(find "${GITHUB_WORKSPACE}" -maxdepth 1 -type f \ + \( -name 'SimpleITK_*.tgz' -o -name 'SimpleITK_*.zip' -o -name 'SimpleITK_*.tar.gz' \) \ + | head -n 1) - # Create output directory for artifacts - mkdir -p artifacts + if [[ -z "${PKG_PATH}" ]]; then + echo "No built package artifact found in ${GITHUB_WORKSPACE}." + exit 1 + fi + + # package naming is SimpleITK_${PKG_VERSION}_R${R_VERSION_SHORT}_${OS_ARCHIVE_EXT} + R_VERSION_SHORT=$(echo "${{ matrix.R }}" | cut -d'.' -f1,2) + PKG_VERSION=$(Rscript -e "cat(read.dcf('DESCRIPTION', 'Version')[1])") - # Create binary package archive from installed package (no recompilation) - cd "${R_LIBS_USER}" if [[ "$RUNNER_OS" == "macOS" ]]; then PKG_NAME="SimpleITK_${PKG_VERSION}_R${R_VERSION_SHORT}_macos-x86_64.tgz" - tar czf "${GITHUB_WORKSPACE}/artifacts/${PKG_NAME}" SimpleITK elif [[ "$RUNNER_OS" == "Linux" ]]; then PKG_NAME="SimpleITK_${PKG_VERSION}_R${R_VERSION_SHORT}_linux-x86_64.tar.gz" - tar czf "${GITHUB_WORKSPACE}/artifacts/${PKG_NAME}" SimpleITK elif [[ "$RUNNER_OS" == "Windows" ]]; then PKG_NAME="SimpleITK_${PKG_VERSION}_R${R_VERSION_SHORT}_windows-x86_64.zip" - 7z a -tzip "${GITHUB_WORKSPACE}/artifacts/${PKG_NAME}" SimpleITK + else + echo "Unsupported OS: $RUNNER_OS" + exit 1 fi - cd "${GITHUB_WORKSPACE}" + mv "${PKG_PATH}" "${GITHUB_WORKSPACE}/artifacts/${PKG_NAME}" ls -lh artifacts/ # Export PKG_NAME as output for use in upload step echo "pkg_name=${PKG_NAME}" >> $GITHUB_OUTPUT - name: Upload binary package - if: steps.create_package.outcome == 'success' + if: steps.rename_package.outcome == 'success' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ steps.create_package.outputs.pkg_name }} + name: ${{ steps.rename_package.outputs.pkg_name }} path: artifacts/* retention-days: 30 @@ -161,8 +173,43 @@ jobs: draft: true name: SimpleITK ${{ github.ref_name }} R Package Release body: | - Please review and test the packages before publishing this release. - Then remove the "Draft" status to make it public and delete this message. + **Please review and test the packages before publishing this release. Then remove this line and make it public.** + + Detailed release notes are available on the [main SimpleITK repository](https://github.com/SimpleITK/SimpleITK/releases/${{ github.ref_name }}). + + The GitHub Pages repository provides platform-specific binaries. If a binary is not available, you will need to build SimpleITK locally using the remotes installer. See the [README](https://github.com/${{ github.repository }}/blob/main/README.md) for detailed instructions. + + Windows and macOS: + + ```r + install.packages( + "SimpleITK", + repos = c("https://simpleitk.github.io/SimpleITKRInstaller/${{ github.ref_name }}"), + type = "binary" + ) + ``` + + Linux: + + ```r + install.packages( + "SimpleITK", + repos = NULL, + contriburl = paste0( + "https://simpleitk.github.io/SimpleITKRInstaller/${{ github.ref_name }}/__linux__/ubuntu-noble/", + paste(R.version$major, sub("\\..*$", "", R.version$minor), sep = "."), + "/src/contrib" + ), + type = "source" + ) + ``` + + The Linux install above uses a "source" type installation even though it is binary, due to the way R handles binary packages on this platform. + + If for some reason you decided to download the package artifact from this release page, before you install the package you will need to first unzip the file. Then rename it to `SimpleITK_.zip` (windows), `SimpleITK_.tgz` (macOS), or `SimpleITK_.tar.gz` (Linux) to match the expected file name format. + + ```r + files: | release-artifacts/* fail_on_unmatched_files: true diff --git a/.github/workflows/update_gh_cran.yml b/.github/workflows/update_gh_cran.yml new file mode 100644 index 0000000..4314395 --- /dev/null +++ b/.github/workflows/update_gh_cran.yml @@ -0,0 +1,59 @@ +name: Update CRAN Repository + +on: + release: + types: [published] + +permissions: + contents: write + +concurrency: + group: update-cran-repository + cancel-in-progress: false + +jobs: + update-repo: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: main + + - name: Set up R + uses: r-lib/actions/setup-r@a51a8012b0aab7c32ef9d19bf54da93f3254335e # v2.12.0 + + - name: Download Release Assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p temp_packages + gh release download ${{ github.event.release.tag_name }} --dir temp_packages + + - name: Route Assets and Build PACKAGES Files + shell: bash + run: | + # Run the R script and always do the cleanup of the temp_packages directory, + # step returns the exit code from the R script. + ( + Rscript update_cran_repo.R \ + --packages_dir temp_packages \ + --base_cran_dir . \ + --repo_url https://github.com/${{ github.repository }} \ + --tag ${{ github.event.release.tag_name }} + status=$? + rm -rf temp_packages + exit $status + ) + + - name: Commit and Push Changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git checkout gh-pages + git add . + if ! git diff --cached --quiet; then + git commit -m "Update CRAN repository index for release ${{ github.event.release.tag_name }}" + git push + fi diff --git a/update_cran_repo.R b/update_cran_repo.R new file mode 100644 index 0000000..94341fb --- /dev/null +++ b/update_cran_repo.R @@ -0,0 +1,246 @@ +#!/usr/bin/env Rscript + +# Script to update CRAN-like repository with a package release which points +# to GitHub release assets. This allows us to maintain a CRAN-like website +# for users to easily install packages while hosting the actual binaries +# on GitHub releases. The script merges new package entries with existing +# ones in the PACKAGES file or creates it if it doesn't exist. +# +# CRAN like directory structure using the posit style structure for linux, treat +# it as if it were a source distribution and not binary +# (https://docs.posit.co/rspm/admin/serving-binaries.html). +# CRAN does not distribute linux binaries, so nothing to mimic there. +# Each version gets its own directory under the base CRAN directory, +# +# vMAJOR.MINOR.PATCH/ +# │ +# ├── __linux__/ (using posit structure for source packages) +# │ ├── ubuntu-noble/ +# │ │ ├── 4.4 +# │ │ └── src/ +# │ │ └── contrib/ +# │ │ ├── PACKAGES +# │ │ ├── PACKAGES.gz +# │ │ ├── PACKAGES.rds +# │ │ └── package_version.tar.gz (omitted in our case) +# │ ├── rhel9/ +# │ ├── 4.4 +# │ └── src/ +# │ └── contrib/ +# │ ├── PACKAGES +# │ ├── PACKAGES.gz +# │ ├── PACKAGES.rds +# │ └── package_version.tar.gz (omitted in our case) +# │ +# ├── bin/ +# ├── windows/ +# │ └── contrib/ +# │ └── 4.4/ +# │ ├── PACKAGES +# │ ├── PACKAGES.gz +# │ ├── PACKAGES.rds +# │ └── package_version.zip (omitted in our case) +# └── macosx/ +# ├── contrib/ (intel) +# │ └── 4.4/ +# │ ├── PACKAGES +# │ ├── PACKAGES.gz +# │ ├── PACKAGES.rds +# │ └── package_version.tgz (omitted in our case) +# ├── big-sur-arm64/ +# │ └── contrib/ +# │ └── 4.4/ +# │ ├── PACKAGES +# │ ├── PACKAGES.gz +# │ ├── PACKAGES.rds +# │ └── package_version.tgz (omitted in our case) +# └── sonoma-arm64/ +# └── contrib/ +# └── 4.6/ +# ├── PACKAGES +# ├── PACKAGES.gz +# ├── PACKAGES.rds +# └── package_version.tgz (omitted in our case) +# +# +# Usage: +# Rscript update_cran_repo.R --packages_dir --base_cran_dir --repo_url --tag +# +# Arguments: +# --packages_dir Directory containing downloaded package files. The package +# files are expected to be named in the format: +# SimpleITK_{VERSION}_R{R_VERSION}_{PLATFORM}.{extension} +# --base_cran_dir Base directory for CRAN-like repository (e.g., "docs" for a GitHub pages site) +# --repo_url URL of the GitHub repository storing the binary files as release assets (e.g., https://github.com/user/repo) +# --tag Release tag name (e.g. v2.5.5) +# +# Example: +# Rscript update_cran_repo.R \ +# --packages_dir temp_packages \ +# --base_cran_dir docs \ +# --repo_url https://github.com/SimpleITK/SimpleITKRInstaller \ +# --tag v2.5.5 + +library(tools) + +# Utility function to merge and sort package entries +merge_packages <- function(existing_packages, new_package, new_url, version) { + # Replace or add "File" field from write_PACKAGES with release URL, + # ensures PACKAGES has a single File entry. + if (!is.null(colnames(new_package)) && "File" %in% colnames(new_package)) { + new_package <- new_package[, colnames(new_package) != "File", drop = FALSE] + } + new_package <- cbind(new_package, File = new_url) + + # Combine with existing packages + if (!is.null(existing_packages) && nrow(existing_packages) > 0) { + # Remove any existing entry for the same version (to handle re-releases) + existing_packages <- existing_packages[existing_packages[, "Version"] != version, , drop = FALSE] + all_packages <- rbind(new_package, existing_packages) + } else { + all_packages <- new_package + } + + # Sort by version (newest first) + if (nrow(all_packages) > 1) { + versions <- package_version(all_packages[, "Version"]) + all_packages <- all_packages[order(versions, decreasing = TRUE), , drop = FALSE] + } + + return(all_packages) +} + +# Parse command-line arguments in --key value format +# Takes a character vector of arguments and returns a named list +# Validates that all required arguments are present and that no consecutive keys are present +# Example: c("--packages_dir", "temp", "--tag", "v1.0") -> list("packages_dir" = "temp", "tag" = "v1.0") +parse_args <- function(args, required_args = NULL) { + parsed <- list() + i <- 1 + while (i <= length(args)) { + if (startsWith(args[i], "--")) { + key <- sub("^--", "", args[i]) + if (i < length(args) && !startsWith(args[i + 1], "--")) { + parsed[[key]] <- args[i + 1] + i <- i + 2 + } else { + stop(sprintf("Missing or invalid value for argument: --%s", key)) + } + } else { + stop(sprintf("Unexpected argument format: %s (expected a key starting with --)", args[i])) + } + } + # Validate required arguments + if (!is.null(required_args)) { + missing_args <- setdiff(required_args, names(parsed)) + if (length(missing_args) > 0) { + stop("Missing required arguments: ", paste(missing_args, collapse = ", ")) + } + } + return(parsed) +} + + + +# Package platform to [repository path, type] mapping +platform_map <- list( + "windows-x86_64" = list(path = "bin/windows/contrib", type = "win.binary"), + "macos-x86_64" = list(path = "bin/macosx/contrib", type = "mac.binary"), + "macos-arm64" = list(path = "bin/macosx/big-sur-arm64/contrib", type = "mac.binary"), + "linux-x86_64" = list(path = "__linux__/ubuntu-noble", type = "source") +) + +# Expected package filename pattern +package_pattern <- "^SimpleITK_([^_]+)_R([0-9]+\\.[0-9]+)_([^\\.]+)\\.(.*)$" + +# Parse command line arguments +args <- commandArgs(trailingOnly = TRUE) +parsed_args <- parse_args(args, required_args = c("packages_dir", "base_cran_dir", "repo_url", "tag")) + +packages_dir <- parsed_args[["packages_dir"]] +base_cran_dir <- parsed_args[["base_cran_dir"]] +repo_url <- parsed_args[["repo_url"]] +tag <- parsed_args[["tag"]] +effective_base_cran_dir <- file.path(base_cran_dir, tag) + +# Validate packages directory exists +if (!dir.exists(packages_dir)) { + quit(save = "no", status = 0) +} + +# Get all files in packages_dir that match the expected pattern +all_files <- list.files(packages_dir, full.names = TRUE) +files <- all_files[grepl(package_pattern, basename(all_files))] + +for (file in files) { + tryCatch({ + filename <- basename(file) + + matches <- regmatches(filename, regexec(package_pattern, filename))[[1]] + version <- matches[2] + r_version <- matches[3] + platform <- matches[4] + extension <- matches[5] + + if (!platform %in% names(platform_map)) { + message("Unknown platform: ", platform) + next + } + + # Create destination directory + platform_info <- platform_map[[platform]] + dest_dir <- if (platform_info$type == "source") { + file.path(effective_base_cran_dir, platform_info$path, r_version, "src", "contrib") + } else { + file.path(effective_base_cran_dir, platform_info$path, r_version) + } + dir.create(dest_dir, recursive = TRUE, showWarnings = FALSE) + + # Read existing PACKAGES file BEFORE it gets overwritten + packages_file <- file.path(dest_dir, "PACKAGES") + existing_packages <- if (file.exists(packages_file)) { + read.dcf(packages_file) + } else { + NULL + } + + # Copy file temporarily to generate PACKAGES metadata + cleaned_name <- sprintf("SimpleITK_%s.%s", version, extension) + temp_dest <- file.path(dest_dir, cleaned_name) + if (!file.copy(file, temp_dest, overwrite = TRUE)) { + stop(sprintf("Failed to copy %s to %s", file, temp_dest)) + } + + # Generate PACKAGES file with metadata from the binary (this overwrites existing) + write_PACKAGES(dest_dir, type = platform_map[[platform]]$type, latestOnly = FALSE) + + # Read the newly generated package entry + new_package <- read.dcf(packages_file) + + # Merge with existing packages and sort, also adds + # the File field using GitHub URL + merged_packages <- merge_packages(existing_packages, + new_package, + sprintf("%s/releases/download/%s/%s", repo_url, tag, filename), + version) + + # Write merged PACKAGES file + write.dcf(merged_packages, packages_file) + + # Generate compressed version + gzf <- gzfile(file.path(dest_dir, "PACKAGES.gz"), "w") + write.dcf(merged_packages, gzf) + close(gzf) + + # Generate PACKAGES.rds version + saveRDS(merged_packages, file.path(dest_dir, "PACKAGES.rds"), version = 2) + + # Remove the temporary binary package file from the CRAN-like directory + unlink(temp_dest) + }, error = function(e) { + message(sprintf("Error processing file %s: %s", file, e$message)) + quit(save = "no", status = 1) + }) +} + + From 01ed6425ed8d75ff16d08deb397869ea86aced37 Mon Sep 17 00:00:00 2001 From: Ziv Yaniv Date: Thu, 11 Jun 2026 12:02:58 -0400 Subject: [PATCH 6/6] Move to using a foyer package to install SimpleITK Previous CRAN-like structure was not working with standard R install tools. The File specification in the PACKAGES file is appended to the CRAN-like's URL which works if the actual package is on the website but specifying a URL as the File value results in an invalid URL structure (https://cran_likehttps://file_location). Moving to a foyer package approach supports download from arbitrary location. The user installs the SimpleITK foyer package using standard R tools (mimicing a SimpleITK install). Then calls the library(SimpleITK); install_simpleitk() which downloads and installs SimpleITK itself, overwritting the foyer package. Next call to library(SimpleITK) loads the actual SimpleITK package. --- .github/workflows/main.yml | 37 ++- .github/workflows/update_gh_cran.yml | 35 +-- SimpleITK_Foyer/DESCRIPTION | 25 ++ SimpleITK_Foyer/NAMESPACE | 3 + SimpleITK_Foyer/R/install.R | 184 ++++++++++++++ SimpleITK_Foyer/R/zzz.R | 29 +++ SimpleITK_Foyer/README.md | 43 ++++ SimpleITK_Foyer/man/Version.Rd | 15 ++ SimpleITK_Foyer/man/get_platform_info.Rd | 15 ++ SimpleITK_Foyer/man/install_simpleitk.Rd | 64 +++++ update_cran_repo.R | 296 ++++++++--------------- 11 files changed, 510 insertions(+), 236 deletions(-) create mode 100644 SimpleITK_Foyer/DESCRIPTION create mode 100644 SimpleITK_Foyer/NAMESPACE create mode 100644 SimpleITK_Foyer/R/install.R create mode 100644 SimpleITK_Foyer/R/zzz.R create mode 100644 SimpleITK_Foyer/README.md create mode 100644 SimpleITK_Foyer/man/Version.Rd create mode 100644 SimpleITK_Foyer/man/get_platform_info.Rd create mode 100644 SimpleITK_Foyer/man/install_simpleitk.Rd diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3deb401..4c5d6c1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -177,38 +177,35 @@ jobs: Detailed release notes are available on the [main SimpleITK repository](https://github.com/SimpleITK/SimpleITK/releases/${{ github.ref_name }}). - The GitHub Pages repository provides platform-specific binaries. If a binary is not available, you will need to build SimpleITK locally using the remotes installer. See the [README](https://github.com/${{ github.repository }}/blob/main/README.md) for detailed instructions. + To install SimpleITK we use a Foyer helper package that downloads the appropriate binary from the GitHub release assets. This is a two step process. - Windows and macOS: + To install the latest SimpleITK version to your primary library directory, first element of `.libPaths()`, run the following: ```r + # install the SimpleITK foyer package install.packages( - "SimpleITK", - repos = c("https://simpleitk.github.io/SimpleITKRInstaller/${{ github.ref_name }}"), - type = "binary" + "SimpleITK.foyer", + repos = c("https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/"), + type = "source" ) - ``` - Linux: + # Use foyer to install SimpleITK + library(SimpleITK.foyer) + install_simpleitk() + ``` + Now you can load the SimpleITK library as usual: ```r - install.packages( - "SimpleITK", - repos = NULL, - contriburl = paste0( - "https://simpleitk.github.io/SimpleITKRInstaller/${{ github.ref_name }}/__linux__/ubuntu-noble/", - paste(R.version$major, sub("\\..*$", "", R.version$minor), sep = "."), - "/src/contrib" - ), - type = "source" - ) + library(SimpleITK) ``` - The Linux install above uses a "source" type installation even though it is binary, due to the way R handles binary packages on this platform. - - If for some reason you decided to download the package artifact from this release page, before you install the package you will need to first unzip the file. Then rename it to `SimpleITK_.zip` (windows), `SimpleITK_.tgz` (macOS), or `SimpleITK_.tar.gz` (Linux) to match the expected file name format. + The `install_simpleitk` function provides finer installation control such as SimpleITK version, library installation location and more. To see all options: ```r + help(install_simpleitk) + ``` + + If you directly download the package artifact from this release page, before you install the package you will need to first unzip the file. Then rename it to `SimpleITK_.zip` (windows), `SimpleITK_.tgz` (macOS), or `SimpleITK_.tar.gz` (Linux) to match the expected file name format. files: | release-artifacts/* diff --git a/.github/workflows/update_gh_cran.yml b/.github/workflows/update_gh_cran.yml index 4314395..5d80979 100644 --- a/.github/workflows/update_gh_cran.yml +++ b/.github/workflows/update_gh_cran.yml @@ -24,36 +24,23 @@ jobs: - name: Set up R uses: r-lib/actions/setup-r@a51a8012b0aab7c32ef9d19bf54da93f3254335e # v2.12.0 - - name: Download Release Assets - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build Foyer Package run: | - mkdir -p temp_packages - gh release download ${{ github.event.release.tag_name }} --dir temp_packages + Rscript update_cran_repo.R \ + --foyer_dir SimpleITK_Foyer \ + --output_dir /tmp/cran_output \ + --repo_url https://github.com/${{ github.repository }} \ + --tag ${{ github.event.release.tag_name }} - - name: Route Assets and Build PACKAGES Files - shell: bash - run: | - # Run the R script and always do the cleanup of the temp_packages directory, - # step returns the exit code from the R script. - ( - Rscript update_cran_repo.R \ - --packages_dir temp_packages \ - --base_cran_dir . \ - --repo_url https://github.com/${{ github.repository }} \ - --tag ${{ github.event.release.tag_name }} - status=$? - rm -rf temp_packages - exit $status - ) - - - name: Commit and Push Changes + - name: Deploy to gh-pages run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git checkout gh-pages - git add . + rm -rf src + cp -r /tmp/cran_output/src . + git add src if ! git diff --cached --quiet; then - git commit -m "Update CRAN repository index for release ${{ github.event.release.tag_name }}" + git commit -m "Update CRAN-like repository for release ${{ github.event.release.tag_name }}" git push fi diff --git a/SimpleITK_Foyer/DESCRIPTION b/SimpleITK_Foyer/DESCRIPTION new file mode 100644 index 0000000..68f260b --- /dev/null +++ b/SimpleITK_Foyer/DESCRIPTION @@ -0,0 +1,25 @@ +Package: SimpleITK.foyer +Version: 0.0.0 +Title: Lightweight Installer for SimpleITK +Authors@R: c(person("Richard", "Beare", role = "aut", + email = "Richard.Beare@ieee.org"), + person("Bradley", "Lowekamp", role = "aut", + email = "blowekamp@mail.nih.gov"), + person("Ziv", "Yaniv", role = c("aut", "cre"), + email = "zivyaniv@nih.gov")) +Description: Provides a lightweight installer for the SimpleITK package, which + is an interface to the Insight Toolkit (ITK) for medical image segmentation + and registration. After installing this package, call install_simpleitk(). + to download and install the pre-built binary packages for your platform + from the SimpleITKRInstaller GitHub releases. Due to the large size of the compiled + package, the actual functionality is distributed as platform-specific binaries + on GitHub. +License: Apache License (>= 2) +URL: https://github.com/SimpleITK/SimpleITKRInstaller +BugReports: https://github.com/SimpleITK/SimpleITKRInstaller/issues +Encoding: UTF-8 +Depends: R (>= 4.0) +Imports: utils +SystemRequirements: Internet connection for downloading binary packages +Maintainer: Ziv Yaniv +Config/roxygen2/version: 8.0.0 diff --git a/SimpleITK_Foyer/NAMESPACE b/SimpleITK_Foyer/NAMESPACE new file mode 100644 index 0000000..0099eb0 --- /dev/null +++ b/SimpleITK_Foyer/NAMESPACE @@ -0,0 +1,3 @@ +export(install_simpleitk) +importFrom(utils, install.packages) +importFrom(utils, download.file) diff --git a/SimpleITK_Foyer/R/install.R b/SimpleITK_Foyer/R/install.R new file mode 100644 index 0000000..17ad46f --- /dev/null +++ b/SimpleITK_Foyer/R/install.R @@ -0,0 +1,184 @@ +# Base URLs for SimpleITK binary releases. +# These values are updated by the build workflow when generating the foyer package. +.sitk_releases_base_url <- "https://github.com/SimpleITK/SimpleITKRInstaller/releases/download" +.sitk_releases_page_url <- "https://github.com/SimpleITK/SimpleITKRInstaller/releases" +.sitk_repo_url <- "https://github.com/SimpleITK/SimpleITKRInstaller" + +#' Install SimpleITK Binary Package +#' +#' Downloads and installs the pre-built SimpleITK binary package for your +#' platform and R version from GitHub releases. +#' +#' @param version Character string specifying the SimpleITK version to install. +#' Defaults to the version of this foyer package. +#' @param lib Character string specifying the library path where the package +#' should be installed. Defaults to the first element of \code{.libPaths()}. +#' @param repos Character string specifying alternative repository URL. +#' By default, uses GitHub releases. +#' @param force Logical. If \code{TRUE}, forces reinstallation even if the +#' package is already installed. +#' @param quiet Logical. If \code{TRUE}, suppresses progress messages. +#' +#' @return Invisibly returns \code{TRUE} if installation succeeds, +#' \code{FALSE} otherwise. +#' +#' @details +#' This function detects your operating system, R version, and architecture, +#' then downloads the appropriate pre-built binary package from the SimpleITK +#' GitHub releases. The binary packages are built by the SimpleITK project +#' and hosted on GitHub releases. +#' +#' Supported platforms: +#' \itemize{ +#' \item Windows x86_64 (R >= 4.0) +#' \item macOS x86_64 (R >= 4.0) +#' \item macOS ARM64 (R >= 4.0) +#' \item Linux x86_64 (R >= 4.0) +#' } +#' +#' @examples +#' \dontrun{ +#' # Install SimpleITK for your platform +#' install_simpleitk() +#' +#' # Install a specific version +#' install_simpleitk(version = "2.5.0") +#' +#' # Force reinstallation +#' install_simpleitk(force = TRUE) +#' } +#' +#' @export +install_simpleitk <- function(version = NULL, + lib = .libPaths()[1], + repos = NULL, + force = FALSE, + quiet = FALSE) { + + # Check if already installed + if (!force && requireNamespace("SimpleITK", quietly = TRUE)) { + if (!quiet) { + message("SimpleITK is already installed. Use force = TRUE to reinstall.") + } + return(invisible(TRUE)) + } + + # Get version from DESCRIPTION if not specified + if (is.null(version)) { + version <- utils::packageDescription("SimpleITK.foyer", fields = "Version") + if (is.na(version)) { + stop("Cannot determine SimpleITK version. Please specify version parameter.") + } + } + + # Get R version (major.minor only) + r_version <- paste(R.version$major, + strsplit(R.version$minor, "\\.")[[1]][1], + sep = ".") + + # Detect platform + platform_info <- get_platform_info() + + if (is.null(platform_info)) { + stop("Unsupported platform. SimpleITK binaries are only available for ", + "Windows (x86_64), macOS (x86_64, ARM64), and Linux (x86_64).") + } + + # Construct download URL + if (is.null(repos)) { + tag <- paste0("v", version) + filename <- sprintf("SimpleITK_%s_R%s_%s.%s", + version, r_version, + platform_info$platform, + platform_info$extension) + download_url <- file.path(.sitk_releases_base_url, tag, filename) + } else { + download_url <- file.path(repos, sprintf("SimpleITK_%s.%s", + version, + platform_info$extension)) + } + + if (!quiet) { + message("Downloading SimpleITK ", version, " for ", platform_info$platform, + " (R ", r_version, ")...") + message("URL: ", download_url) + } + + # Create temporary directory and file with proper naming + # R expects package files to be named: PackageName_Version.extension + temp_dir <- tempfile() + dir.create(temp_dir, showWarnings = FALSE) + on.exit(unlink(temp_dir, recursive = TRUE), add = TRUE) + + pkg_filename <- sprintf("SimpleITK_%s.%s", version, platform_info$extension) + temp_file <- file.path(temp_dir, pkg_filename) + + # Download the binary + tryCatch({ + download.file(download_url, temp_file, mode = "wb", quiet = quiet) + }, error = function(e) { + stop("Failed to download SimpleITK binary package.\n", + "URL: ", download_url, "\n", + "Error: ", conditionMessage(e), "\n", + "Please check that:\n", + " 1. You have an internet connection\n", + " 2. The specified version (", version, ") has pre-built binaries\n", + " 3. A binary exists for your platform and R version\n", + "Available releases: ", .sitk_releases_page_url) + }) + + if (!quiet) { + message("Installing SimpleITK package...") + } + + # Install the binary package + tryCatch({ + install.packages(temp_file, repos = NULL, type = "source", lib = lib, + quiet = quiet) + + if (!quiet) { + message("SimpleITK successfully installed!") + message("Load it with: library(SimpleITK)") + } + return(invisible(TRUE)) + + }, error = function(e) { + stop("Failed to install SimpleITK binary package.\n", + "Error: ", conditionMessage(e), "\n", + "You may need to build from source instead, see ", .sitk_repo_url, ".\n") + }) +} + + +#' Get Platform Information +#' +#' @return A list with platform and extension, or NULL if unsupported +#' @keywords internal +get_platform_info <- function() { + os <- Sys.info()["sysname"] + arch <- Sys.info()["machine"] + + # Support both x86_64 and ARM64 architectures + # Note: Windows may report "x86-64" (with hyphen) or "x86_64" (with underscore) + is_x86_64 <- grepl("x86[_-]64|amd64", arch, ignore.case = TRUE) + is_arm64 <- grepl("arm64|aarch64", arch, ignore.case = TRUE) + + if (!is_x86_64 && !is_arm64) { + return(NULL) + } + + if (os == "Windows") { + return(list(platform = "windows-x86_64", extension = "zip")) + } else if (os == "Darwin") { + # macOS: distinguish between x86_64 and ARM64 + if (is_arm64) { + return(list(platform = "macos-arm64", extension = "tgz")) + } else { + return(list(platform = "macos-x86_64", extension = "tgz")) + } + } else if (os == "Linux") { + return(list(platform = "linux-x86_64", extension = "tar.gz")) + } else { + return(NULL) + } +} diff --git a/SimpleITK_Foyer/R/zzz.R b/SimpleITK_Foyer/R/zzz.R new file mode 100644 index 0000000..4c5575d --- /dev/null +++ b/SimpleITK_Foyer/R/zzz.R @@ -0,0 +1,29 @@ +#' @keywords internal +.onAttach <- function(libname, pkgname) { + # Check if the full SimpleITK binary is installed + full_pkg_installed <- tryCatch({ + # Check if the package exists and has the actual SimpleITK functionality + pkg_path <- system.file(package = "SimpleITK", lib.loc = libname) + if (pkg_path != "") { + # Check if it has the core SimpleITK functions (not just this foyer) + ns <- loadNamespace("SimpleITK") + exists("Image", envir = ns, mode = "function") + } else { + FALSE + } + }, error = function(e) FALSE) + + if (!full_pkg_installed) { + packageStartupMessage( + "================================================================================\n", + "Welcome to SimpleITK installer!\n\n", + "The SimpleITK package is not yet installed.\n", + "To download and install the latest pre-built binary for your platform, run:\n\n", + " install_simpleitk()\n\n", + "This will download a platform-specific binary from GitHub releases.\n", + "After installation, load the package with: library(SimpleITK)\n", + "To download and install a specific SimpleITK version, use: install_simpleitk(version = 'x.y.z')\n", + "================================================================================" + ) + } +} diff --git a/SimpleITK_Foyer/README.md b/SimpleITK_Foyer/README.md new file mode 100644 index 0000000..bb3a00c --- /dev/null +++ b/SimpleITK_Foyer/README.md @@ -0,0 +1,43 @@ +# SimpleITK Foyer Package + +This is a lightweight installer package for SimpleITK, providing easy access to pre-built SimpleITK binaries for R. + +## Installation + +Install the SimpleITK foyer package from GitHub Pages: +```r +install.packages("SimpleITK.foyer", + repos = "https://SimpleITK.github.io/SimpleITKRInstaller", + type = "source") +``` + +Then download and install the **latest** binary package: +```r +library(SimpleITK.foyer) +install_simpleitk() +``` + +or a specific version: +```r +library(SimpleITK.foyer) +install_simpleitk(version = "2.5.5") +``` + +Old versions may not be available, previously not distributed as binaries. In this case you will need to build SimpleITK locally [using the SimpleITKRInstaller](https://github.com/SimpleITK/SimpleITKRInstaller). + +After installtion, use SimpleITK as usual: + +```r +library(SimpleITK) + +img <- ReadImage("path/to/image.dcm") +``` + +## Rational for using Foyer Package + +The full SimpleITK package is quite large (~40-50 MB depending on platform) and requires significant compilation time when built from source. To make it easier for users, we distribute: + +1. **This lightweight foyer package** - hosted on GitHub Pages as a CRAN-like repository +2. **Pre-built binaries on GitHub Releases** - the actual SimpleITK package + +The foyer package installs instantly and provides the `install_simpleitk()` function which downloads the correct pre-built binary for your platform. diff --git a/SimpleITK_Foyer/man/Version.Rd b/SimpleITK_Foyer/man/Version.Rd new file mode 100644 index 0000000..ea4b6d3 --- /dev/null +++ b/SimpleITK_Foyer/man/Version.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{Version} +\alias{Version} +\title{SimpleITK Version Information (Stub)} +\usage{ +Version() +} +\value{ +NULL if SimpleITK is not installed, otherwise calls the real Version function +} +\description{ +This is a stub function that will be replaced by the actual function +once the full SimpleITK package is installed via \code{install_simpleitk()}. +} diff --git a/SimpleITK_Foyer/man/get_platform_info.Rd b/SimpleITK_Foyer/man/get_platform_info.Rd new file mode 100644 index 0000000..2bbcd3d --- /dev/null +++ b/SimpleITK_Foyer/man/get_platform_info.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{get_platform_info} +\alias{get_platform_info} +\title{Get Platform Information} +\usage{ +get_platform_info() +} +\value{ +A list with platform and extension, or NULL if unsupported +} +\description{ +Get Platform Information +} +\keyword{internal} diff --git a/SimpleITK_Foyer/man/install_simpleitk.Rd b/SimpleITK_Foyer/man/install_simpleitk.Rd new file mode 100644 index 0000000..05aef6e --- /dev/null +++ b/SimpleITK_Foyer/man/install_simpleitk.Rd @@ -0,0 +1,64 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/install.R +\name{install_simpleitk} +\alias{install_simpleitk} +\title{Install SimpleITK Binary Package} +\usage{ +install_simpleitk( + version = NULL, + lib = .libPaths()[1], + repos = NULL, + force = FALSE, + quiet = FALSE +) +} +\arguments{ +\item{version}{Character string specifying the SimpleITK version to install. +Defaults to the version of this foyer package.} + +\item{lib}{Character string specifying the library path where the package +should be installed. Defaults to the first element of \code{.libPaths()}.} + +\item{repos}{Character string specifying alternative repository URL. +By default, uses GitHub releases.} + +\item{force}{Logical. If \code{TRUE}, forces reinstallation even if the +package is already installed.} + +\item{quiet}{Logical. If \code{TRUE}, suppresses progress messages.} +} +\value{ +Invisibly returns \code{TRUE} if installation succeeds, + \code{FALSE} otherwise. +} +\description{ +Downloads and installs the pre-built SimpleITK binary package for your +platform and R version from GitHub releases. +} +\details{ +This function detects your operating system, R version, and architecture, +then downloads the appropriate pre-built binary package from the SimpleITK +GitHub releases. The binary packages are built by the SimpleITK project +and hosted at: \url{https://github.com/SimpleITK/SimpleITKRInstaller/releases} + +Supported platforms: +\itemize{ + \item Windows x86_64 (R >= 4.0) + \item macOS x86_64 (R >= 4.0) + \item macOS ARM64 (R >= 4.0) + \item Linux x86_64 (R >= 4.0) +} +} +\examples{ +\dontrun{ +# Install SimpleITK for your platform +install_simpleitk() + +# Install a specific version +install_simpleitk(version = "2.5.0") + +# Force reinstallation +install_simpleitk(force = TRUE) +} + +} diff --git a/update_cran_repo.R b/update_cran_repo.R index 94341fb..aec53b7 100644 --- a/update_cran_repo.R +++ b/update_cran_repo.R @@ -1,119 +1,43 @@ #!/usr/bin/env Rscript -# Script to update CRAN-like repository with a package release which points -# to GitHub release assets. This allows us to maintain a CRAN-like website -# for users to easily install packages while hosting the actual binaries -# on GitHub releases. The script merges new package entries with existing -# ones in the PACKAGES file or creates it if it doesn't exist. +# Script to build the SimpleITK foyer package and deploy it to a CRAN-like +# repository structure on gh-pages. The foyer is a lightweight source package +# that, once installed, downloads the actual SimpleITK binary from GitHub +# releases for the user's platform. # -# CRAN like directory structure using the posit style structure for linux, treat -# it as if it were a source distribution and not binary -# (https://docs.posit.co/rspm/admin/serving-binaries.html). -# CRAN does not distribute linux binaries, so nothing to mimic there. -# Each version gets its own directory under the base CRAN directory, +# The gh-pages CRAN-like structure (source packages only): # -# vMAJOR.MINOR.PATCH/ -# │ -# ├── __linux__/ (using posit structure for source packages) -# │ ├── ubuntu-noble/ -# │ │ ├── 4.4 -# │ │ └── src/ -# │ │ └── contrib/ -# │ │ ├── PACKAGES -# │ │ ├── PACKAGES.gz -# │ │ ├── PACKAGES.rds -# │ │ └── package_version.tar.gz (omitted in our case) -# │ ├── rhel9/ -# │ ├── 4.4 -# │ └── src/ -# │ └── contrib/ -# │ ├── PACKAGES -# │ ├── PACKAGES.gz -# │ ├── PACKAGES.rds -# │ └── package_version.tar.gz (omitted in our case) -# │ -# ├── bin/ -# ├── windows/ -# │ └── contrib/ -# │ └── 4.4/ -# │ ├── PACKAGES -# │ ├── PACKAGES.gz -# │ ├── PACKAGES.rds -# │ └── package_version.zip (omitted in our case) -# └── macosx/ -# ├── contrib/ (intel) -# │ └── 4.4/ -# │ ├── PACKAGES -# │ ├── PACKAGES.gz -# │ ├── PACKAGES.rds -# │ └── package_version.tgz (omitted in our case) -# ├── big-sur-arm64/ -# │ └── contrib/ -# │ └── 4.4/ -# │ ├── PACKAGES -# │ ├── PACKAGES.gz -# │ ├── PACKAGES.rds -# │ └── package_version.tgz (omitted in our case) -# └── sonoma-arm64/ -# └── contrib/ -# └── 4.6/ -# ├── PACKAGES -# ├── PACKAGES.gz -# ├── PACKAGES.rds -# └── package_version.tgz (omitted in our case) +# src/ +# └── contrib/ +# ├── PACKAGES +# ├── PACKAGES.gz +# ├── PACKAGES.rds +# └── SimpleITK.foyer_.tar.gz (updated with each release) # +# The foyer package is version-aware and updated with each release. +# Users install it once from a fixed URL, then use install_simpleitk(version=...) +# to download any specific binary version from GitHub Releases. # # Usage: -# Rscript update_cran_repo.R --packages_dir --base_cran_dir --repo_url --tag +# Rscript update_cran_repo.R --foyer_dir --output_dir --repo_url --tag # # Arguments: -# --packages_dir Directory containing downloaded package files. The package -# files are expected to be named in the format: -# SimpleITK_{VERSION}_R{R_VERSION}_{PLATFORM}.{extension} -# --base_cran_dir Base directory for CRAN-like repository (e.g., "docs" for a GitHub pages site) -# --repo_url URL of the GitHub repository storing the binary files as release assets (e.g., https://github.com/user/repo) -# --tag Release tag name (e.g. v2.5.5) +# --foyer_dir Directory containing the foyer package template +# --output_dir Output directory for the CRAN-like structure (e.g., /tmp/cran_output) +# --repo_url URL of the GitHub repository hosting binary releases +# (e.g., https://github.com/SimpleITK/SimpleITKRInstaller) +# --tag Release tag name (e.g., v2.5.5) # # Example: # Rscript update_cran_repo.R \ -# --packages_dir temp_packages \ -# --base_cran_dir docs \ +# --foyer_dir SimpleITK_Foyer \ +# --output_dir /tmp/cran_output \ # --repo_url https://github.com/SimpleITK/SimpleITKRInstaller \ # --tag v2.5.5 library(tools) -# Utility function to merge and sort package entries -merge_packages <- function(existing_packages, new_package, new_url, version) { - # Replace or add "File" field from write_PACKAGES with release URL, - # ensures PACKAGES has a single File entry. - if (!is.null(colnames(new_package)) && "File" %in% colnames(new_package)) { - new_package <- new_package[, colnames(new_package) != "File", drop = FALSE] - } - new_package <- cbind(new_package, File = new_url) - - # Combine with existing packages - if (!is.null(existing_packages) && nrow(existing_packages) > 0) { - # Remove any existing entry for the same version (to handle re-releases) - existing_packages <- existing_packages[existing_packages[, "Version"] != version, , drop = FALSE] - all_packages <- rbind(new_package, existing_packages) - } else { - all_packages <- new_package - } - - # Sort by version (newest first) - if (nrow(all_packages) > 1) { - versions <- package_version(all_packages[, "Version"]) - all_packages <- all_packages[order(versions, decreasing = TRUE), , drop = FALSE] - } - - return(all_packages) -} - # Parse command-line arguments in --key value format -# Takes a character vector of arguments and returns a named list -# Validates that all required arguments are present and that no consecutive keys are present -# Example: c("--packages_dir", "temp", "--tag", "v1.0") -> list("packages_dir" = "temp", "tag" = "v1.0") parse_args <- function(args, required_args = NULL) { parsed <- list() i <- 1 @@ -130,7 +54,6 @@ parse_args <- function(args, required_args = NULL) { stop(sprintf("Unexpected argument format: %s (expected a key starting with --)", args[i])) } } - # Validate required arguments if (!is.null(required_args)) { missing_args <- setdiff(required_args, names(parsed)) if (length(missing_args) > 0) { @@ -140,107 +63,96 @@ parse_args <- function(args, required_args = NULL) { return(parsed) } - - -# Package platform to [repository path, type] mapping -platform_map <- list( - "windows-x86_64" = list(path = "bin/windows/contrib", type = "win.binary"), - "macos-x86_64" = list(path = "bin/macosx/contrib", type = "mac.binary"), - "macos-arm64" = list(path = "bin/macosx/big-sur-arm64/contrib", type = "mac.binary"), - "linux-x86_64" = list(path = "__linux__/ubuntu-noble", type = "source") -) - -# Expected package filename pattern -package_pattern <- "^SimpleITK_([^_]+)_R([0-9]+\\.[0-9]+)_([^\\.]+)\\.(.*)$" - # Parse command line arguments args <- commandArgs(trailingOnly = TRUE) -parsed_args <- parse_args(args, required_args = c("packages_dir", "base_cran_dir", "repo_url", "tag")) +parsed_args <- parse_args(args, required_args = c("foyer_dir", "output_dir", "repo_url", "tag")) -packages_dir <- parsed_args[["packages_dir"]] -base_cran_dir <- parsed_args[["base_cran_dir"]] +foyer_dir <- parsed_args[["foyer_dir"]] +output_dir <- parsed_args[["output_dir"]] repo_url <- parsed_args[["repo_url"]] tag <- parsed_args[["tag"]] -effective_base_cran_dir <- file.path(base_cran_dir, tag) +version <- sub("^v", "", tag) -# Validate packages directory exists -if (!dir.exists(packages_dir)) { - quit(save = "no", status = 0) +# Validate foyer directory exists +if (!dir.exists(foyer_dir)) { + stop("Foyer directory does not exist: ", foyer_dir) } -# Get all files in packages_dir that match the expected pattern -all_files <- list.files(packages_dir, full.names = TRUE) -files <- all_files[grepl(package_pattern, basename(all_files))] - -for (file in files) { - tryCatch({ - filename <- basename(file) - - matches <- regmatches(filename, regexec(package_pattern, filename))[[1]] - version <- matches[2] - r_version <- matches[3] - platform <- matches[4] - extension <- matches[5] - - if (!platform %in% names(platform_map)) { - message("Unknown platform: ", platform) - next - } - - # Create destination directory - platform_info <- platform_map[[platform]] - dest_dir <- if (platform_info$type == "source") { - file.path(effective_base_cran_dir, platform_info$path, r_version, "src", "contrib") - } else { - file.path(effective_base_cran_dir, platform_info$path, r_version) - } - dir.create(dest_dir, recursive = TRUE, showWarnings = FALSE) - - # Read existing PACKAGES file BEFORE it gets overwritten - packages_file <- file.path(dest_dir, "PACKAGES") - existing_packages <- if (file.exists(packages_file)) { - read.dcf(packages_file) - } else { - NULL - } - - # Copy file temporarily to generate PACKAGES metadata - cleaned_name <- sprintf("SimpleITK_%s.%s", version, extension) - temp_dest <- file.path(dest_dir, cleaned_name) - if (!file.copy(file, temp_dest, overwrite = TRUE)) { - stop(sprintf("Failed to copy %s to %s", file, temp_dest)) - } - - # Generate PACKAGES file with metadata from the binary (this overwrites existing) - write_PACKAGES(dest_dir, type = platform_map[[platform]]$type, latestOnly = FALSE) - - # Read the newly generated package entry - new_package <- read.dcf(packages_file) - - # Merge with existing packages and sort, also adds - # the File field using GitHub URL - merged_packages <- merge_packages(existing_packages, - new_package, - sprintf("%s/releases/download/%s/%s", repo_url, tag, filename), - version) - - # Write merged PACKAGES file - write.dcf(merged_packages, packages_file) - - # Generate compressed version - gzf <- gzfile(file.path(dest_dir, "PACKAGES.gz"), "w") - write.dcf(merged_packages, gzf) - close(gzf) - - # Generate PACKAGES.rds version - saveRDS(merged_packages, file.path(dest_dir, "PACKAGES.rds"), version = 2) - - # Remove the temporary binary package file from the CRAN-like directory - unlink(temp_dest) - }, error = function(e) { - message(sprintf("Error processing file %s: %s", file, e$message)) - quit(save = "no", status = 1) - }) +# Copy foyer template to a temp directory to avoid modifying the original +build_dir <- tempdir() +foyer_copy <- file.path(build_dir, "SimpleITK") +if (dir.exists(foyer_copy)) unlink(foyer_copy, recursive = TRUE) +dir.create(foyer_copy, recursive = TRUE) +file.copy(list.files(foyer_dir, full.names = TRUE), foyer_copy, recursive = TRUE) + +# 1. Update DESCRIPTION version +desc_path <- file.path(foyer_copy, "DESCRIPTION") +desc_lines <- readLines(desc_path) +desc_lines <- sub("^Version:.*", sprintf("Version: %s", version), desc_lines) +# Add/update Date field +date_line <- sprintf("Date: %s", Sys.Date()) +if (any(grepl("^Date:", desc_lines))) { + desc_lines <- sub("^Date:.*", date_line, desc_lines) +} else { + # Insert Date after Version + version_idx <- grep("^Version:", desc_lines) + desc_lines <- append(desc_lines, date_line, after = version_idx) } +writeLines(desc_lines, desc_path) + +# 2. Update repository URLs in R/install.R +install_r_path <- file.path(foyer_copy, "R", "install.R") +install_lines <- readLines(install_r_path) +releases_url <- paste0(repo_url, "/releases/download") +releases_page <- paste0(repo_url, "/releases") + +# Update the three URL variables +install_lines <- sub( + '^(\\.sitk_releases_base_url <- ").*(")', + paste0("\\1", releases_url, "\\2"), + install_lines +) + +install_lines <- sub( + '^(\\.sitk_releases_page_url <- ").*(")', + paste0("\\1", releases_page, "\\2"), + install_lines +) + +install_lines <- sub( + '^(\\.sitk_repo_url <- ").*(")', + paste0("\\1", repo_url, "\\2"), + install_lines +) + +writeLines(install_lines, install_r_path) + +# 3. Build the source package +message("Building foyer package...") +# Set tar options to avoid uid/gid warnings when R CMD build creates the tarball. +# --no-same-owner prevents tar from trying to preserve file ownership information, +# which can cause warnings if the original uid/gid values don't exist in the build +# environment. The package installs correctly regardless of these attributes. +Sys.setenv(R_BUILD_TAR = "tar --no-same-owner") +build_output <- system2("R", args = c("CMD", "build", "--no-manual", foyer_copy), + stdout = TRUE, stderr = TRUE) +cat(build_output, sep = "\n") + +tarball <- sprintf("SimpleITK.foyer_%s.tar.gz", version) +if (!file.exists(tarball)) { + stop("R CMD build failed. Expected tarball not found: ", tarball) +} + +# 4. Create CRAN-like directory structure +dest_dir <- file.path(output_dir, "src", "contrib") +dir.create(dest_dir, recursive = TRUE, showWarnings = FALSE) +file.rename(tarball, file.path(dest_dir, tarball)) + +# 5. Generate PACKAGES files +write_PACKAGES(dest_dir, type = "source") + +message("CRAN-like repository created at: ", output_dir) +message(" ", file.path(dest_dir, tarball)) +message(" ", file.path(dest_dir, tarball))