libR-sys 0.7.1

Low level bindings to the R programming language.
Documentation
name: Tests

on:
  push:
    branches:
      - main
      - master
  pull_request:
    branches:
      - main
      - master
  issue_comment:
    types:
      - created
  # Run this once per three days
  schedule:
    - cron: '29 17 */3 * *'
  # This can also manually run
  workflow_dispatch: {}

jobs:

  test_with_bindgen:
    # When the event is not issue_comment, always run the tests. When it is,
    # check if (1) the comment is on pull request, (2) the comment author is the
    # member of the organization, and (3) the comment starts with '/bindings'.
    #
    # ref.
    # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment
    if: |
      github.event_name != 'issue_comment'
      || (github.event.issue.pull_request && github.event.comment.author_association == 'MEMBER' && startsWith(github.event.comment.body, '/bindings'))

    runs-on: ${{ matrix.config.os }}

    name: ${{ matrix.config.os }} (R-${{ matrix.config.r }} rust-${{ matrix.config.rust-version }}-${{ matrix.config.target || 'default' }})

    strategy:
      fail-fast: false
      matrix:
        config:
          # On Windows,
          #
          # * for R >= 4.2, both the MSVC toolchain and the GNU toolchain should
          #   work. Since we primarily support MSVC, we focus on testing MSVC
          #   here. Also, at least one GNU case should be included so that we
          #   can detect when something gets broken. 
          # * for R < 4.2, the MSVC toolchain must be used to support
          #   cross-compilation to 32-bit. 
          #
          # options:
          #   - targets: Targets to build and run tests against. If not
          #     specified, 'default' will be used. 
          #   - no-test-targets: Targets to skip tests.
          #   - emit-bindings: If 'true', emit bindings. In principle, we choose
          #     only one stable Rust toolchain per combination of a platform and
          #     an R version (e.g. Windows and R-release) to emit bindings.
          - {os: windows-latest, r: 'release', rust-version: 'stable-msvc',  target: 'x86_64-pc-windows-gnu', emit-bindings: 'true'}
          - {os: windows-latest, r: 'release', rust-version: 'nightly-msvc', target: 'x86_64-pc-windows-gnu'}
          - {os: windows-latest, r: 'devel',   rust-version: 'stable-msvc',  target: 'x86_64-pc-windows-gnu', emit-bindings: 'true'}
          - {os: windows-latest, r: 'release', rust-version: 'stable-gnu',   target: 'x86_64-pc-windows-gnu'}
          - {os: windows-latest, r: 'oldrel',  rust-version: 'stable-msvc',  target: 'x86_64-pc-windows-gnu', emit-bindings: 'true'}
          - {os: windows-latest, r: '4.2',     rust-version: 'stable-msvc',  target: 'x86_64-pc-windows-gnu', emit-bindings: 'true' }

          - {os: macOS-latest,   r: 'release', rust-version: 'nightly'}
          - {os: macOS-latest,   r: 'devel',   rust-version: 'stable',  emit-bindings: 'true'}
          - {os: macOS-latest,   r: 'oldrel',  rust-version: 'stable',  emit-bindings: 'true'}
          - {os: macOS-latest,   r: 'release', rust-version: 'stable', emit-bindings: 'true'}
          - {os: macOS-latest,   r: 'release', rust-version: 'stable', target: 'x86_64-apple-darwin', skip-tests: 'true', emit-bindings: 'true'}
          - {os: macOS-latest,   r: '4.2',     rust-version: 'stable', emit-bindings: 'true' }
          - {os: macOS-latest,   r: '4.2',     rust-version: 'stable', target: 'x86_64-apple-darwin', skip-tests: 'true', emit-bindings: 'true'}

          - {os: ubuntu-latest,   r: 'release', rust-version: 'nightly'}
          - {os: ubuntu-latest,   r: 'release', rust-version: 'stable', emit-bindings: 'true'}
          - {os: ubuntu-latest,   r: 'release', rust-version: 'stable', target: 'aarch64-unknown-linux-gnu', skip-tests: 'true', emit-bindings: 'true'}

          - {os: ubuntu-latest,   r: 'devel',   rust-version: 'stable', emit-bindings: 'true'}
          - {os: ubuntu-latest,   r: 'devel',   rust-version: 'stable', target: 'aarch64-unknown-linux-gnu', skip-tests: 'true', emit-bindings: 'true'}

          - {os: ubuntu-latest,   r: 'oldrel',  rust-version: 'stable', emit-bindings: 'true'}
          - {os: ubuntu-latest,   r: 'oldrel',  rust-version: 'stable', target: 'aarch64-unknown-linux-gnu', skip-tests: 'true', emit-bindings: 'true'}

          - {os: ubuntu-latest,   r: '4.2',     rust-version: 'stable', emit-bindings: 'true' }
          - {os: ubuntu-latest,   r: '4.2',     rust-version: 'stable', target: 'aarch64-unknown-linux-gnu', skip-tests: 'true', emit-bindings: 'true'}
    env:
      RSPM: ${{ matrix.config.rspm }}

    # PowerShell core is available on all platforms and can be used to unify scripts
    defaults:
      run:
        shell: pwsh

    steps:

      - uses: actions/checkout@v4

      # When invoked by an issue comment event, the GitHub Actions runner runs
      # on the default branch, so we need to switch the branch of the pull
      # request. Since the branch name is not easily accessible via variables
      # provided GitHub Actions, we use r-lib/actions, which is well-maintained.
      - name: Switch branch (/bindings command)
        if: github.event_name == 'issue_comment'
        uses: r-lib/actions/pr-fetch@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Set up R
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: ${{ matrix.config.r }}
          use-public-rspm: true

      - name: Set up Rust
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.config.rust-version }}
          components: rustfmt, clippy
          targets: ${{ matrix.config.target }}

      # All configurations for Windows go here
      # 1. Configure linker
      # 2. Create libgcc_eh mock
      # 3. Add R bin path to PATH
      # 4. Add Rtools' GCC to PATH (required to find linker)
      # 5. Add include path (required to resolve standard headers like stdio.h)
      - name: Configure Windows
        if: runner.os == 'Windows'
        run: |
          # Configure linker
          echo "RUSTFLAGS=-C linker=x86_64-w64-mingw32.static.posix-gcc.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

          # Create libgcc_eh mock
          New-Item -Path libgcc_mock -Type Directory
          New-Item -Path libgcc_mock\libgcc_eh.a -Type File
          New-Item -Path libgcc_mock\libgcc_s.a -Type File
          $pwd_slash = echo "${PWD}" | % {$_ -replace '\\','/'}
          echo "LIBRARY_PATH=${pwd_slash}/libgcc_mock" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

          # Add R bin path to PATH
          echo "$(Rscript.exe --vanilla -e 'cat(normalizePath(R.home()))')\bin\x64"  | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append ;

          # Add Rtools' GCC to PATH
          # Source: https://github.com/r-lib/actions/blob/b7e68d63e51bdf225997973e2add36d551f60f02/setup-r/lib/installer.js#L471
          $directories = @(
            "C:\rtools44-aarch64\aarch64-w64-mingw32.static.posix",
            "C:\rtools44\x86_64-w64-mingw32.static.posix",
            "C:\rtools43\x86_64-w64-mingw32.static.posix",
            "C:\rtools42\x86_64-w64-mingw32.static.posix",
            "C:\rtools40\ucrt64",
            "C:\Rtools\mingw_64"
          )

          $mingw_root = $null
          foreach ($dir in $directories) {
              if (Test-Path $dir) {
                  $mingw_root = $dir
                  Write-Host "Found Rtools directory at: $mingw_root"
                  break
              }
          }
          echo "MINGW_ROOT=$mingw_root" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

          if ($null -eq $mingw_root) {
              Write-Host "No Rtools directory found."
          } else {
              Write-Host "Mingw root set to: $mingw_root"
          }
          echo "$mingw_root\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

          # Add include path
          echo "LIBRSYS_LIBCLANG_INCLUDE_PATH=$mingw_root\include" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
        env:
          RUST_TARGET: ${{ matrix.config.target }}

      # macOS configurations, mainly llvm and path to libclang
      # Because of this R installation issue on macOS-11.0 
      # https://github.com/r-lib/actions/issues/200
      # Symlinks to R/Rscript are not properly set up, so we do it by hand, using this trick
      # https://github.com/r-lib/ps/commit/a24f2c4d1bdba63be14e7729b9ab81d0ed9f719e
      # Environment variables are required fir Mac-OS-11.0, see
      # https://github.com/extendr/libR-sys/issues/35
      - name: Configure macOS
        if: runner.os == 'macOS'
        run: |
          brew install llvm
          echo "LIBCLANG_PATH=$(brew --prefix llvm)/lib" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          $env:LLVM_CONFIG_PATH = "$(brew --prefix llvm)/bin/llvm-config" 
          echo "LLVM_CONFIG_PATH=$env:LLVM_CONFIG_PATH" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
          echo "LIBRSYS_LIBCLANG_INCLUDE_PATH=$(. $env:LLVM_CONFIG_PATH --libdir)/clang/$(. $env:LLVM_CONFIG_PATH --version)/include"  | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

          if ((Get-ChildItem -Path /usr/local/bin -Filter R | Measure-Object).Count -eq 0) {
            echo "::warning:: Found no R symlink in /usr/local/bin, setting up manually..."
            ln -s /Library/Frameworks/R.framework/Versions/Current/Resources/bin/R /usr/local/bin/
          }
          if ((Get-ChildItem -Path /usr/local/bin -Filter Rscript | Measure-Object).Count -eq 0) {
            echo "::warning:: Found no Rscript symlink in /usr/local/bin, setting up manually..."
            ln -s /Library/Frameworks/R.framework/Versions/Current/Resources/bin/Rscript /usr/local/bin/
          }

      # This is required for ubuntu r-devel
      # 'Del alias:R' removes R alias which prevents running R 
      - name: Configure Linux
        if: runner.os == 'linux'
        run: |
          Del alias:R
          echo "LD_LIBRARY_PATH=$(R -s -e 'cat(normalizePath(R.home()))')/lib" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

          if($env:RUST_TARGET -eq 'aarch64-unknown-linux-gnu') {
            sudo apt-get update
            sudo apt-get install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
            # https://github.com/rust-lang/rust-bindgen/issues/1229
            echo 'BINDGEN_EXTRA_CLANG_ARGS=--sysroot=/usr/aarch64-linux-gnu' >> $GITHUB_ENV
            # https://github.com/rust-lang/rust/issues/28924
            echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc' >> $GITHUB_ENV
          }
        env:
          RUST_TARGET: ${{ matrix.config.target }}

      
      # Build & run bindings with layout tests
      - name: Run tests
        id: test
        if: matrix.config.skip-tests != 'true'
        run: |
          . ./ci-cargo.ps1
          # ci-cargo test -vv --features use-bindgen,layout_tests $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) '--' --nocapture -ActionName "Running bindgen tests for target: $env:RUST_TARGET"
          # ci-cargo test -vv --features use-bindgen,non-api,layout_tests $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) '--' --nocapture -ActionName "Running bindgen tests for target: $env:RUST_TARGET (with non-API)"
          ci-cargo test -vv $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) '--' --nocapture -ActionName "Running bindgen tests for target: $env:RUST_TARGET"
        env:
          RUST_TARGET: ${{ matrix.config.target }}

      # Build and emit bindings to './generated_bindings'
      - name: Build & Emit bindings
        id: build
        run: |
          . ./ci-cargo.ps1
          # ci-cargo build -vv --features use-bindgen $(if ($env:RUST_TARGET -ne '') {"--target=$env:RUST_TARGET"} ) -ActionName "Building for target: $env:RUST_TARGET"
        env:
          LIBRSYS_BINDINGS_OUTPUT_PATH: generated_bindings
          RUST_TARGET: ${{ matrix.config.target }}

      - name: Upload generated bindings
        if:
          steps.build.outcome == 'success' &&
          matrix.config.emit-bindings == 'true'
        uses: actions/upload-artifact@v4
        with:
          name: generated_binding-${{ matrix.config.os }}-R-${{ matrix.config.r }}-rust-${{ matrix.config.rust-version }}-${{ matrix.config.target || 'default'}}
          path: generated_bindings
  
  check_generate_bindings_flag:
    name: Check if [generate bindings] is in latest commit message
    runs-on: ubuntu-latest
    outputs:
      head_commit_message: ${{ steps.get_head_commit_message.outputs.HEAD_COMMIT_MESSAGE }}
      # generate_bindings: ${{ contains(steps.get_head_commit_message.outputs.HEAD_COMMIT_MESSAGE, '[generate bindings]') }}
    steps:
    - uses: actions/checkout@v4
      with:
        ref: ${{ github.event.pull_request.head.sha }}
    - name: Get Head Commit Message
      id: get_head_commit_message
      run: echo "HEAD_COMMIT_MESSAGE=$(git show -s --format=%s)" >> "$GITHUB_OUTPUT"
    - name: Show commit message
      run: |
        echo "${{ steps.get_head_commit_message.outputs.HEAD_COMMIT_MESSAGE }}"
        echo "${{ contains(steps.get_head_commit_message.outputs.HEAD_COMMIT_MESSAGE, '[generate bindings]') }}"
  
  pr_generated_bindings:
    name: Make PR with generated bindings
    needs: [test_with_bindgen, check_generate_bindings_flag]
    if: ${{ contains(needs.check_generate_bindings_flag.outputs.head_commit_message, '[generate bindings]') }}
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        ref: ${{ github.event.pull_request.head.ref }}
    - uses: actions/download-artifact@v4

    - name: Update bindings
      run: |
        # Update or add the bindings
        cp generated_binding-*/*.rs bindings/

        # Replace the default bindings
        cd bindings
        for x in linux-aarch64 linux-x86_64 macos-aarch64 macos-x86_64 windows-x86_64; do
          # Choose the newest version except for devel
          ln --force -s "$(ls -1 ./bindings-${x}-*.rs | grep -v devel | sort | tail -1)" ./bindings-${x}.rs
        done
        cd ..
    - name: Add generated bindings
      run: |
        git add bindings/
        git config --local user.name "${GITHUB_ACTOR}"
        git config --local user.email "${GITHUB_ACTOR}@users.noreply.github.com"
        git commit -m "Update bindings [skip ci]"
    - name: Push to PR branch
      run: git push

  # Gather the generated bindings and push them to generated_bindings branch.
  # If we need to update the bindings, create a pull request from that branch.
  commit_generated_bindings:
    needs: test_with_bindgen
    runs-on: ubuntu-latest
    # In the case of /bindings command, we don't need to check anything else
    # because it should have checked in test_with_bindings job. In the other
    # cases, we only want to invoke this on the master branch.
    if: github.event_name == 'issue_comment' || github.ref == 'refs/heads/master'
    steps:
    - uses: actions/checkout@v4

    - uses: actions/download-artifact@v4

    - name: Switch branch
      if: github.event_name != 'issue_comment'
      run: |
        # 1) If there's already generated_bindings branch, checkout it.
        # 2) If generated_binding branch is not created, create it from the default branch.
        if git ls-remote --exit-code --heads origin generated_bindings 2>&1 >/dev/null; then
          git fetch origin --no-tags --prune --depth=1 generated_bindings
          git checkout generated_bindings
        else
          git switch -c generated_bindings
        fi

    - name: Switch branch (/bindings command)
      if: github.event_name == 'issue_comment'
      uses: r-lib/actions/pr-fetch@v2
      with:
        repo-token: ${{ secrets.GITHUB_TOKEN }}

    - name: Commit the generated bindings
      run: |
        # Update or add the bindings
        cp generated_binding-*/*.rs bindings/

        # Replace the default bindings
        cd bindings
        for x in linux-aarch64 linux-x86_64 macos-aarch64 macos-x86_64 windows-x86_64; do
          # Choose the newest version except for devel
          ln --force -s "$(ls -1 ./bindings-${x}-*.rs | grep -v devel | sort | tail -1)" ./bindings-${x}.rs
        done
        cd ..

        # detect changes (the code is derived from https://stackoverflow.com/a/3879077)
        git add bindings/
        git update-index --refresh
        if ! git diff-index --quiet HEAD -- bindings/; then
          git config --local user.name "${GITHUB_ACTOR}"
          git config --local user.email "${GITHUB_ACTOR}@users.noreply.github.com"
          git commit -m "Update bindings [skip ci]"
        else
          echo "No changes"
        fi

    - name: Push
      if: github.event_name != 'issue_comment'
      run: git push origin generated_bindings

    - name: Push (/bindings command)
      if: github.event_name == 'issue_comment'
      uses: r-lib/actions/pr-push@v2
      with:
        repo-token: ${{ secrets.GITHUB_TOKEN }}