├── .Rbuildignore ├── .aviator └── config.yml ├── .gitattributes ├── .github ├── .gitignore ├── CODE_OF_CONDUCT.md └── workflows │ ├── R-CMD-check-dev.yaml │ ├── R-CMD-check-status.yaml │ ├── R-CMD-check.yaml │ ├── check │ └── action.yml │ ├── commit │ └── action.yml │ ├── covr │ └── action.yml │ ├── dep-matrix │ └── action.yml │ ├── dep-suggests-matrix │ ├── action.R │ └── action.yml │ ├── fledge.yaml │ ├── get-extra │ └── action.yml │ ├── git-identity │ └── action.yml │ ├── install │ └── action.yml │ ├── lock.yaml │ ├── matrix-check │ └── action.yml │ ├── pkgdown-build │ └── action.yml │ ├── pkgdown-deploy │ └── action.yml │ ├── pkgdown.yaml │ ├── pr-commands.yaml │ ├── rate-limit │ └── action.yml │ ├── revdep.yaml │ ├── roxygenize │ └── action.yml │ ├── style │ └── action.yml │ ├── update-snapshots │ └── action.yml │ └── versions-matrix │ ├── action.R │ └── action.yml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── absolute.R ├── criterion.R ├── deprecated.R ├── file.R ├── path.R ├── root.R ├── rprojroot-package.R ├── shortcut.R ├── thisfile.R └── utils.R ├── README.Rmd ├── README.md ├── TODO.md ├── _pkgdown.yml ├── codecov.yml ├── cran-comments.md ├── index.md ├── inst └── WORDLIST ├── man ├── criteria.Rd ├── deprecated.Rd ├── figures │ ├── lifecycle-archived.svg │ ├── lifecycle-defunct.svg │ ├── lifecycle-deprecated.svg │ ├── lifecycle-experimental.svg │ ├── lifecycle-maturing.svg │ ├── lifecycle-questioning.svg │ ├── lifecycle-soft-deprecated.svg │ ├── lifecycle-stable.svg │ └── lifecycle-superseded.svg ├── find_root.Rd ├── find_root_file.Rd ├── root_criterion.Rd ├── rprojroot-package.Rd └── thisfile.Rd ├── revdep ├── .gitignore ├── README.md └── problems.md ├── rprojroot.Rproj ├── tests ├── testthat.R └── testthat │ ├── _snaps │ ├── root.md │ └── testthat.md │ ├── hierarchy │ ├── DESCRIPTION │ ├── a │ │ ├── b │ │ │ ├── a │ │ │ ├── b │ │ │ ├── c │ │ │ │ └── d │ │ │ └── d │ │ │ │ └── e │ │ └── remake.yml │ ├── b │ ├── c │ └── hierarchy.Rproj │ ├── package │ ├── DESCRIPTION │ └── tests │ │ ├── testthat.R │ │ └── testthat │ │ ├── .gitignore │ │ └── test-something.R │ ├── scripts │ ├── thisfile-cat.R │ ├── thisfile.R │ └── thisfile.Rmd │ ├── setup.R │ ├── startup.Rs │ ├── test-absolute.R │ ├── test-criterion.R │ ├── test-file.R │ ├── test-path.R │ ├── test-root.R │ ├── test-testthat.R │ ├── test-utils.R │ └── vcs │ ├── git.zip │ └── svn.zip └── vignettes └── rprojroot.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^rprojroot\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.gitignore$ 4 | ^cran-comments\.md$ 5 | ^docs$ 6 | ^_pkgdown\.yml$ 7 | ^revdep$ 8 | ^tests/testthat/hierarchy/a/b/\.projectile$ 9 | ^LICENSE\.md$ 10 | ^\.github$ 11 | ^README\.Rmd$ 12 | ^TODO\.md$ 13 | ^CRAN-RELEASE$ 14 | ^codecov\.yml$ 15 | ^CRAN-SUBMISSION$ 16 | ^\.aviator/config\.yml$ 17 | ^index\.md$ 18 | -------------------------------------------------------------------------------- /.aviator/config.yml: -------------------------------------------------------------------------------- 1 | merge_rules: 2 | labels: 3 | trigger: mergequeue 4 | skip_line: mergequeue-priority 5 | merge_failed: blocked 6 | skip_delete_branch: '' 7 | update_latest: true 8 | delete_branch: false 9 | use_rebase: true 10 | publish_status_check: ready 11 | status_comment: 12 | publish: always 13 | open_message: '' 14 | queued_message: '' 15 | blocked_message: '' 16 | enable_comments: true 17 | ci_timeout_mins: 0 18 | require_all_checks_pass: true 19 | require_skip_line_reason: false 20 | preconditions: 21 | validations: [] 22 | number_of_approvals: 0 23 | required_checks: [] 24 | use_github_mergeability: true 25 | conversation_resolution_required: false 26 | merge_mode: 27 | type: default 28 | auto_update: 29 | enabled: true 30 | label: 'mergequeue' 31 | max_runs_for_update: 0 32 | merge_commit: 33 | use_title_and_body: false 34 | merge_strategy: 35 | name: merge 36 | override_labels: 37 | squash: '' 38 | merge: '' 39 | rebase: '' 40 | base_branches: 41 | - main 42 | scenarios: [] 43 | version: 1.1.0 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /NEWS.md merge=union 2 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | R-version 3 | depends.rds 4 | /pkg.lock 5 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who 4 | contribute through reporting issues, posting feature requests, updating documentation, 5 | submitting pull requests or patches, and other activities. 6 | 7 | We are committed to making participation in this project a harassment-free experience for 8 | everyone, regardless of level of experience, gender, gender identity and expression, 9 | sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 10 | 11 | Examples of unacceptable behavior by participants include the use of sexual language or 12 | imagery, derogatory comments or personal attacks, trolling, public or private harassment, 13 | insults, or other unprofessional conduct. 14 | 15 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 16 | commits, code, wiki edits, issues, and other contributions that are not aligned to this 17 | Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed 18 | from the project team. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 21 | opening an issue or contacting one or more of the project maintainers. 22 | 23 | This Code of Conduct is adapted from the Contributor Covenant 24 | (http://contributor-covenant.org), version 1.0.0, available at 25 | http://contributor-covenant.org/version/1/0/0/ 26 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check-dev.yaml: -------------------------------------------------------------------------------- 1 | # This workflow calls the GitHub API very frequently. 2 | # Can't be run as part of commits 3 | on: 4 | schedule: 5 | - cron: "0 5 * * *" # 05:00 UTC every day only run on main branch 6 | push: 7 | branches: 8 | - "cran-*" 9 | tags: 10 | - "v*" 11 | 12 | name: rcc dev 13 | 14 | jobs: 15 | matrix: 16 | runs-on: ubuntu-22.04 17 | outputs: 18 | matrix: ${{ steps.set-matrix.outputs.matrix }} 19 | 20 | name: Collect deps 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: ./.github/workflows/rate-limit 26 | with: 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - uses: r-lib/actions/setup-r@v2 30 | 31 | - id: set-matrix 32 | uses: ./.github/workflows/dep-matrix 33 | 34 | check-matrix: 35 | runs-on: ubuntu-22.04 36 | needs: matrix 37 | 38 | name: Check deps 39 | 40 | steps: 41 | - name: Install json2yaml 42 | run: | 43 | sudo npm install -g json2yaml 44 | 45 | - name: Check matrix definition 46 | run: | 47 | matrix='${{ needs.matrix.outputs.matrix }}' 48 | echo $matrix 49 | echo $matrix | jq . 50 | echo $matrix | json2yaml 51 | 52 | R-CMD-check-base: 53 | runs-on: ubuntu-22.04 54 | 55 | name: base 56 | 57 | # Begin custom: services 58 | # End custom: services 59 | 60 | strategy: 61 | fail-fast: false 62 | 63 | steps: 64 | - uses: actions/checkout@v4 65 | 66 | - uses: ./.github/workflows/custom/before-install 67 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 68 | 69 | - uses: ./.github/workflows/install 70 | with: 71 | cache-version: rcc-dev-base-1 72 | needs: build, check 73 | extra-packages: "any::rcmdcheck any::remotes ." 74 | token: ${{ secrets.GITHUB_TOKEN }} 75 | 76 | - name: Session info 77 | run: | 78 | options(width = 100) 79 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 80 | pkgs <- installed.packages()[, "Package"] 81 | sessioninfo::session_info(pkgs, include_base = TRUE) 82 | shell: Rscript {0} 83 | 84 | - uses: ./.github/workflows/custom/after-install 85 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 86 | 87 | - uses: ./.github/workflows/update-snapshots 88 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 89 | 90 | - uses: ./.github/workflows/check 91 | with: 92 | results: ${{ matrix.package }} 93 | 94 | R-CMD-check-dev: 95 | needs: 96 | - matrix 97 | - R-CMD-check-base 98 | 99 | runs-on: ubuntu-22.04 100 | 101 | name: 'rcc-dev: ${{ matrix.package }}' 102 | 103 | # Begin custom: services 104 | # End custom: services 105 | 106 | strategy: 107 | fail-fast: false 108 | matrix: ${{fromJson(needs.matrix.outputs.matrix)}} 109 | 110 | steps: 111 | - uses: actions/checkout@v4 112 | 113 | - uses: ./.github/workflows/custom/before-install 114 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 115 | 116 | - uses: ./.github/workflows/install 117 | with: 118 | cache-version: rcc-dev-${{ matrix.package }}-1 119 | needs: build, check 120 | extra-packages: "any::rcmdcheck any::remotes ." 121 | token: ${{ secrets.GITHUB_TOKEN }} 122 | 123 | - name: Install dev version of ${{ matrix.package }} 124 | env: 125 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 126 | run: | 127 | remotes::install_dev("${{ matrix.package }}", "https://cloud.r-project.org", upgrade = "always") 128 | shell: Rscript {0} 129 | 130 | - name: Session info 131 | run: | 132 | options(width = 100) 133 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 134 | pkgs <- installed.packages()[, "Package"] 135 | sessioninfo::session_info(pkgs, include_base = TRUE) 136 | shell: Rscript {0} 137 | 138 | - uses: ./.github/workflows/custom/after-install 139 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 140 | 141 | - uses: ./.github/workflows/update-snapshots 142 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 143 | 144 | - uses: ./.github/workflows/check 145 | with: 146 | results: ${{ matrix.package }} 147 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check-status.yaml: -------------------------------------------------------------------------------- 1 | # Workflow to update the status of a commit for the R-CMD-check workflow 2 | # Necessary because remote PRs cannot update the status of the commit 3 | on: 4 | workflow_run: 5 | workflows: 6 | - rcc 7 | types: 8 | - requested 9 | - completed 10 | 11 | name: rcc-status 12 | 13 | jobs: 14 | rcc-status: 15 | runs-on: ubuntu-24.04 16 | 17 | name: "Update commit status" 18 | 19 | permissions: 20 | contents: read 21 | statuses: write 22 | 23 | steps: 24 | - name: "Update commit status" 25 | # Only run if triggered by rcc workflow 26 | if: github.event.workflow_run.name == 'rcc' 27 | env: 28 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: | 30 | set -x 31 | 32 | if [ "${{ github.event.workflow_run.status }}" == "completed" ]; then 33 | if [ "${{ github.event.workflow_run.conclusion }}" == "success" ]; then 34 | state="success" 35 | else 36 | state="failure" 37 | fi 38 | 39 | # Read artifact ID 40 | artifact_id=$(gh api \ 41 | -H "Accept: application/vnd.github+json" \ 42 | -H "X-GitHub-Api-Version: 2022-11-28" \ 43 | repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}/artifacts | jq -r '.artifacts[] | select(.name == "rcc-smoke-sha") | .id') 44 | 45 | if [ -n "${artifact_id}" ]; then 46 | # Download artifact 47 | curl -L -o rcc-smoke-sha.zip \ 48 | -H "Accept: application/vnd.github+json" \ 49 | -H "Authorization: Bearer ${GH_TOKEN}" \ 50 | -H "X-GitHub-Api-Version: 2022-11-28" \ 51 | https://api.github.com/repos/${{ github.repository }}/actions/artifacts/${artifact_id}/zip 52 | 53 | # Unzip artifact 54 | unzip rcc-smoke-sha.zip 55 | 56 | # Read artifact 57 | sha=$(cat rcc-smoke-sha.txt) 58 | 59 | # Clean up 60 | rm rcc-smoke-sha.zip rcc-smoke-sha.txt 61 | fi 62 | else 63 | state="pending" 64 | fi 65 | 66 | if [ -z "${sha}" ]; then 67 | sha=${{ github.event.workflow_run.head_sha }} 68 | fi 69 | 70 | html_url=${{ github.event.workflow_run.html_url }} 71 | description=${{ github.event.workflow_run.name }} 72 | 73 | gh api \ 74 | --method POST \ 75 | -H "Accept: application/vnd.github+json" \ 76 | -H "X-GitHub-Api-Version: 2022-11-28" \ 77 | repos/${{ github.repository }}/statuses/${sha} \ 78 | -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" 79 | shell: bash 80 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # NOTE: This workflow is overkill for most R packages and 5 | # check-standard.yaml is likely a better choice. 6 | # usethis::use_github_action("check-standard") will install it. 7 | on: 8 | push: 9 | branches: 10 | - main 11 | - master 12 | - release 13 | - cran-* 14 | pull_request: 15 | branches: 16 | - main 17 | - master 18 | workflow_dispatch: 19 | inputs: 20 | ref: 21 | description: "Branch, tag, or commit to check out" 22 | required: false 23 | default: "main" 24 | versions-matrix: 25 | description: "Create a matrix of R versions" 26 | type: boolean 27 | default: false 28 | dep-suggests-matrix: 29 | description: "Create a matrix of suggested dependencies" 30 | type: boolean 31 | default: false 32 | merge_group: 33 | types: 34 | - checks_requested 35 | schedule: 36 | - cron: "10 1 * * *" 37 | 38 | concurrency: 39 | group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.ref || github.head_ref || github.sha }}-${{ github.base_ref || '' }} 40 | cancel-in-progress: true 41 | 42 | name: rcc 43 | 44 | jobs: 45 | rcc-smoke: 46 | runs-on: ubuntu-24.04 47 | 48 | outputs: 49 | sha: ${{ steps.commit.outputs.sha }} 50 | versions-matrix: ${{ steps.versions-matrix.outputs.matrix }} 51 | dep-suggests-matrix: ${{ steps.dep-suggests-matrix.outputs.matrix }} 52 | 53 | name: "Smoke test: stock R" 54 | 55 | permissions: 56 | contents: write 57 | statuses: write 58 | pull-requests: write 59 | actions: write 60 | 61 | # Begin custom: services 62 | # End custom: services 63 | 64 | steps: 65 | - uses: actions/checkout@v4 66 | with: 67 | ref: ${{ inputs.ref }} 68 | 69 | - name: Update status for rcc 70 | # FIXME: Wrap into action 71 | if: github.event_name == 'workflow_dispatch' 72 | env: 73 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | run: | 75 | # Check status of this workflow 76 | state="pending" 77 | sha=${{ inputs.ref }} 78 | if [ -z "${sha}" ]; then 79 | sha=${{ github.head_ref }} 80 | fi 81 | if [ -z "${sha}" ]; then 82 | sha=${{ github.sha }} 83 | fi 84 | sha=$(git rev-parse ${sha}) 85 | 86 | html_url=$(gh api \ 87 | -H "Accept: application/vnd.github+json" \ 88 | -H "X-GitHub-Api-Version: 2022-11-28" \ 89 | repos/${{ github.repository }}/actions/runs/${{ github.run_id }} | jq -r .html_url) 90 | 91 | description="${{ github.workflow }} / ${{ github.job }}" 92 | 93 | gh api \ 94 | --method POST \ 95 | -H "Accept: application/vnd.github+json" \ 96 | -H "X-GitHub-Api-Version: 2022-11-28" \ 97 | repos/${{ github.repository }}/statuses/${sha} \ 98 | -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" 99 | shell: bash 100 | 101 | - uses: ./.github/workflows/rate-limit 102 | with: 103 | token: ${{ secrets.GITHUB_TOKEN }} 104 | 105 | - uses: ./.github/workflows/git-identity 106 | 107 | - uses: ./.github/workflows/custom/before-install 108 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 109 | 110 | - uses: ./.github/workflows/install 111 | with: 112 | token: ${{ secrets.GITHUB_TOKEN }} 113 | cache-version: rcc-smoke-2 114 | needs: build, check, website 115 | # Beware of using dev pkgdown here, has brought in dev dependencies in the past 116 | extra-packages: any::rcmdcheck r-lib/roxygen2 any::decor r-lib/styler r-lib/pkgdown deps::. 117 | 118 | - uses: ./.github/workflows/custom/after-install 119 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 120 | 121 | # Must come after the custom after-install workflow 122 | - name: Install package 123 | run: | 124 | _R_SHLIB_STRIP_=true R CMD INSTALL . 125 | shell: bash 126 | 127 | - id: versions-matrix 128 | # Only run for pull requests if the base repo is different from the head repo, not for workflow_dispatch if not requested, always run for other events 129 | if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository) && (github.event_name != 'workflow_dispatch' || inputs.versions-matrix) 130 | uses: ./.github/workflows/versions-matrix 131 | 132 | - id: dep-suggests-matrix 133 | # Not for workflow_dispatch if not requested, always run for other events 134 | if: github.event_name != 'workflow_dispatch' || inputs.dep-suggests-matrix 135 | uses: ./.github/workflows/dep-suggests-matrix 136 | 137 | - uses: ./.github/workflows/update-snapshots 138 | with: 139 | base: ${{ inputs.ref || github.head_ref }} 140 | 141 | - uses: ./.github/workflows/style 142 | 143 | - uses: ./.github/workflows/roxygenize 144 | 145 | - name: Remove config files from previous iteration 146 | run: | 147 | rm -f .github/dep-suggests-matrix.json .github/versions-matrix.json 148 | shell: bash 149 | 150 | - id: commit 151 | uses: ./.github/workflows/commit 152 | with: 153 | token: ${{ secrets.GITHUB_TOKEN }} 154 | 155 | - uses: ./.github/workflows/check 156 | with: 157 | results: ${{ runner.os }}-smoke-test 158 | 159 | - uses: ./.github/workflows/pkgdown-build 160 | if: github.event_name != 'push' 161 | 162 | - uses: ./.github/workflows/pkgdown-deploy 163 | if: github.event_name == 'push' 164 | 165 | # Upload sha as artifact 166 | - run: | 167 | echo -n "${{ steps.commit.outputs.sha }}" > rcc-smoke-sha.txt 168 | shell: bash 169 | 170 | - uses: actions/upload-artifact@v4 171 | with: 172 | name: rcc-smoke-sha 173 | path: rcc-smoke-sha.txt 174 | 175 | - name: Update status for rcc 176 | # FIXME: Wrap into action 177 | if: always() && github.event_name == 'workflow_dispatch' 178 | env: 179 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 180 | run: | 181 | # Check status of this workflow 182 | if [ "${{ job.status }}" == "success" ]; then 183 | state="success" 184 | else 185 | state="failure" 186 | fi 187 | 188 | sha=${{ steps.commit.outputs.sha }} 189 | if [ -z "${sha}" ]; then 190 | sha=${{ inputs.ref }} 191 | fi 192 | if [ -z "${sha}" ]; then 193 | sha=${{ github.head_ref }} 194 | fi 195 | if [ -z "${sha}" ]; then 196 | sha=${{ github.sha }} 197 | fi 198 | sha=$(git rev-parse ${sha}) 199 | 200 | html_url=$(gh api \ 201 | -H "Accept: application/vnd.github+json" \ 202 | -H "X-GitHub-Api-Version: 2022-11-28" \ 203 | repos/${{ github.repository }}/actions/runs/${{ github.run_id }} | jq -r .html_url) 204 | 205 | description="${{ github.workflow }} / ${{ github.job }}" 206 | 207 | gh api \ 208 | --method POST \ 209 | -H "Accept: application/vnd.github+json" \ 210 | -H "X-GitHub-Api-Version: 2022-11-28" \ 211 | repos/${{ github.repository }}/statuses/${sha} \ 212 | -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" 213 | shell: bash 214 | 215 | rcc-smoke-check-matrix: 216 | runs-on: ubuntu-24.04 217 | 218 | name: "Check matrix" 219 | 220 | needs: 221 | - rcc-smoke 222 | 223 | steps: 224 | - uses: actions/checkout@v4 225 | with: 226 | ref: ${{ needs.rcc-smoke.outputs.sha }} 227 | 228 | - uses: ./.github/workflows/matrix-check 229 | with: 230 | matrix: ${{ needs.rcc-smoke.outputs.versions-matrix }} 231 | 232 | - uses: ./.github/workflows/matrix-check 233 | with: 234 | matrix: ${{ needs.rcc-smoke.outputs.dep-suggests-matrix }} 235 | 236 | rcc-full: 237 | needs: 238 | - rcc-smoke 239 | 240 | runs-on: ${{ matrix.os }} 241 | 242 | if: ${{ needs.rcc-smoke.outputs.versions-matrix != '' }} 243 | 244 | name: 'rcc: ${{ matrix.os }} (${{ matrix.r }}) ${{ matrix.desc }}' 245 | 246 | # Begin custom: services 247 | # End custom: services 248 | 249 | strategy: 250 | fail-fast: false 251 | matrix: ${{fromJson(needs.rcc-smoke.outputs.versions-matrix)}} 252 | 253 | steps: 254 | - uses: actions/checkout@v4 255 | with: 256 | ref: ${{ needs.rcc-smoke.outputs.sha }} 257 | 258 | - uses: ./.github/workflows/custom/before-install 259 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 260 | 261 | - uses: ./.github/workflows/install 262 | with: 263 | r-version: ${{ matrix.r }} 264 | cache-version: rcc-full-1 265 | token: ${{ secrets.GITHUB_TOKEN }} 266 | needs: build, check 267 | 268 | - uses: ./.github/workflows/custom/after-install 269 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 270 | 271 | - name: Must allow NOTEs if packages are missing, even with _R_CHECK_FORCE_SUGGESTS_ 272 | run: | 273 | if (Sys.getenv("RCMDCHECK_ERROR_ON") %in% c("", "note")) { 274 | pkgs <- setdiff(desc::desc_get_deps()$package, "R") 275 | installable <- vapply(pkgs, FUN.VALUE = logical(1), requireNamespace, quietly = TRUE) 276 | if (any(!installable)) { 277 | message("Missing packages: ", paste(pkgs[!installable], collapse = ", ")) 278 | cat('RCMDCHECK_ERROR_ON="warning"\n', file = Sys.getenv("GITHUB_ENV"), append = TRUE) 279 | } 280 | } 281 | shell: Rscript {0} 282 | 283 | - uses: ./.github/workflows/update-snapshots 284 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 285 | 286 | - uses: ./.github/workflows/check 287 | if: ${{ ! matrix.covr }} 288 | with: 289 | results: ${{ runner.os }}-r${{ matrix.r }} 290 | 291 | - uses: ./.github/workflows/covr 292 | if: ${{ matrix.covr }} 293 | with: 294 | token: ${{ secrets.CODECOV_TOKEN }} 295 | 296 | # The status update is taken care of by R-CMD-check-status.yaml 297 | 298 | rcc-suggests: 299 | needs: 300 | - rcc-smoke 301 | 302 | runs-on: ubuntu-22.04 303 | 304 | if: ${{ needs.rcc-smoke.outputs.dep-suggests-matrix != '' }} 305 | 306 | name: Without ${{ matrix.package }} 307 | 308 | # Begin custom: services 309 | # End custom: services 310 | 311 | strategy: 312 | fail-fast: false 313 | matrix: ${{fromJson(needs.rcc-smoke.outputs.dep-suggests-matrix)}} 314 | 315 | steps: 316 | - uses: actions/checkout@v4 317 | 318 | - uses: ./.github/workflows/custom/before-install 319 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 320 | 321 | - uses: ./.github/workflows/install 322 | with: 323 | cache-version: rcc-dev-${{ matrix.package }}-1 324 | needs: build, check 325 | extra-packages: "any::rcmdcheck any::remotes ." 326 | token: ${{ secrets.GITHUB_TOKEN }} 327 | 328 | - name: Remove ${{ matrix.package }} and all strong dependencies 329 | run: | 330 | pkg <- "${{ matrix.package }}" 331 | pkgs <- tools::package_dependencies(pkg, reverse = TRUE)[[1]] 332 | installed <- rownames(utils::installed.packages()) 333 | to_remove <- c(pkg, intersect(pkgs, installed)) 334 | print(to_remove) 335 | remove.packages(to_remove) 336 | shell: Rscript {0} 337 | 338 | - name: Session info 339 | run: | 340 | options(width = 100) 341 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 342 | pkgs <- installed.packages()[, "Package"] 343 | sessioninfo::session_info(pkgs, include_base = TRUE) 344 | shell: Rscript {0} 345 | 346 | - uses: ./.github/workflows/custom/after-install 347 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 348 | 349 | - name: Must allow NOTEs, even with _R_CHECK_FORCE_SUGGESTS_ 350 | run: | 351 | if (Sys.getenv("RCMDCHECK_ERROR_ON") %in% c("", "note")) { 352 | cat('RCMDCHECK_ERROR_ON="warning"\n', file = Sys.getenv("GITHUB_ENV"), append = TRUE) 353 | } 354 | shell: Rscript {0} 355 | 356 | - name: Check env vars 357 | run: | 358 | print(Sys.getenv('_R_CHECK_FORCE_SUGGESTS_')) 359 | print(Sys.getenv('RCMDCHECK_ERROR_ON')) 360 | shell: Rscript {0} 361 | 362 | - uses: ./.github/workflows/check 363 | with: 364 | results: ${{ matrix.package }} 365 | 366 | # The status update is taken care of by R-CMD-check-status.yaml 367 | -------------------------------------------------------------------------------- /.github/workflows/check/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to check an R package" 2 | inputs: 3 | results: 4 | description: Slug for check results 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - uses: r-lib/actions/check-r-package@v2 11 | with: 12 | # Fails on R 3.6 on Windows, remove when this job is removed? 13 | args: 'c("--no-manual", "--as-cran", "--no-multiarch")' 14 | error-on: ${{ env.RCMDCHECK_ERROR_ON || '"note"' }} 15 | 16 | - name: Show test output 17 | if: always() 18 | run: | 19 | ## -- Show test output -- 20 | echo "::group::Test output" 21 | find check -name '*.Rout*' -exec head -n 1000000 '{}' \; || true 22 | echo "::endgroup::" 23 | shell: bash 24 | 25 | - name: Upload check results 26 | if: failure() 27 | uses: actions/upload-artifact@main 28 | with: 29 | name: ${{ inputs.results }}-results 30 | path: check 31 | -------------------------------------------------------------------------------- /.github/workflows/commit/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to commit changes to the repository" 2 | inputs: 3 | token: 4 | description: "GitHub token" 5 | required: true 6 | outputs: 7 | sha: 8 | description: "SHA of generated commit" 9 | value: ${{ steps.commit.outputs.sha }} 10 | 11 | runs: 12 | using: "composite" 13 | steps: 14 | - name: Commit if changed, create a PR if protected 15 | id: commit 16 | env: 17 | GITHUB_TOKEN: ${{ inputs.token }} 18 | run: | 19 | set -x 20 | if [ -n "$(git status --porcelain)" ]; then 21 | echo "Changed" 22 | protected=${{ github.ref_protected }} 23 | foreign=${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} 24 | if [ "${foreign}" = "true" ]; then 25 | # https://github.com/krlmlr/actions-sync/issues/44 26 | echo "Can't push to foreign branch" 27 | elif [ "${protected}" = "true" ]; then 28 | current_branch=$(git branch --show-current) 29 | new_branch=gha-commit-$(git rev-parse --short HEAD) 30 | git checkout -b ${new_branch} 31 | git add . 32 | git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 33 | # Force-push, used in only one place 34 | # Alternative: separate branch names for each usage 35 | git push -u origin HEAD -f 36 | 37 | existing_pr=$(gh pr list --state open --base main --head ${new_branch} --json number --jq '.[] | .number') 38 | if [ -n "${existing_pr}" ]; then 39 | echo "Existing PR: ${existing_pr}" 40 | else 41 | gh pr create --base main --head ${new_branch} --title "chore: Auto-update from GitHub Actions" --body "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 42 | fi 43 | 44 | gh workflow run rcc -f ref=$(git rev-parse HEAD) 45 | gh pr merge --merge --auto 46 | else 47 | git fetch 48 | if [ -n "${GITHUB_HEAD_REF}" ]; then 49 | git add . 50 | git stash save 51 | git switch ${GITHUB_HEAD_REF} 52 | git merge origin/${GITHUB_BASE_REF} --no-edit 53 | git stash pop 54 | fi 55 | git add . 56 | git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 57 | git push -u origin HEAD 58 | 59 | # Only set output if changed 60 | echo sha=$(git rev-parse HEAD) >> $GITHUB_OUTPUT 61 | fi 62 | fi 63 | shell: bash 64 | -------------------------------------------------------------------------------- /.github/workflows/covr/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to run covr for an R package" 2 | inputs: 3 | token: 4 | description: codecov token 5 | required: false 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Run coverage check 11 | run: | 12 | if (dir.exists("tests/testthat")) { 13 | cov <- covr::package_coverage( 14 | quiet = FALSE, 15 | clean = FALSE, 16 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 17 | ) 18 | covr::to_cobertura(cov) 19 | } else { 20 | message("No tests found, coverage not tested.") 21 | } 22 | shell: Rscript {0} 23 | 24 | - uses: codecov/codecov-action@v5 25 | with: 26 | # Fail if token is given 27 | fail_ci_if_error: ${{ inputs.token != '' }} 28 | files: ./cobertura.xml 29 | plugins: noop 30 | disable_search: true 31 | token: ${{ inputs.token }} 32 | 33 | - name: Show testthat output 34 | if: always() 35 | run: | 36 | ## -------------------------------------------------------------------- 37 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 38 | shell: bash 39 | 40 | - name: Upload test results 41 | if: failure() 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: coverage-test-failures 45 | path: ${{ runner.temp }}/package 46 | -------------------------------------------------------------------------------- /.github/workflows/dep-matrix/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to compute a matrix with all dependent packages" 2 | outputs: 3 | matrix: 4 | description: "Generated matrix" 5 | value: ${{ steps.set-matrix.outputs.matrix }} 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - id: set-matrix 11 | run: | 12 | # Determine package dependencies 13 | # From remotes 14 | read_dcf <- function(path) { 15 | fields <- colnames(read.dcf(path)) 16 | as.list(read.dcf(path, keep.white = fields)[1, ]) 17 | } 18 | 19 | re_match <- function(text, pattern, perl = TRUE, ...) { 20 | 21 | stopifnot(is.character(pattern), length(pattern) == 1, !is.na(pattern)) 22 | text <- as.character(text) 23 | 24 | match <- regexpr(pattern, text, perl = perl, ...) 25 | 26 | start <- as.vector(match) 27 | length <- attr(match, "match.length") 28 | end <- start + length - 1L 29 | 30 | matchstr <- substring(text, start, end) 31 | matchstr[ start == -1 ] <- NA_character_ 32 | 33 | res <- data.frame( 34 | stringsAsFactors = FALSE, 35 | .text = text, 36 | .match = matchstr 37 | ) 38 | 39 | if (!is.null(attr(match, "capture.start"))) { 40 | 41 | gstart <- attr(match, "capture.start") 42 | glength <- attr(match, "capture.length") 43 | gend <- gstart + glength - 1L 44 | 45 | groupstr <- substring(text, gstart, gend) 46 | groupstr[ gstart == -1 ] <- NA_character_ 47 | dim(groupstr) <- dim(gstart) 48 | 49 | res <- cbind(groupstr, res, stringsAsFactors = FALSE) 50 | } 51 | 52 | names(res) <- c(attr(match, "capture.names"), ".text", ".match") 53 | class(res) <- c("tbl_df", "tbl", class(res)) 54 | res 55 | } 56 | 57 | dev_split_ref <- function(x) { 58 | re_match(x, "^(?[^@#]+)(?[@#].*)?$") 59 | } 60 | 61 | has_dev_dep <- function(package) { 62 | cran_url <- "https://cloud.r-project.org" 63 | 64 | refs <- dev_split_ref(package) 65 | url <- file.path(cran_url, "web", "packages", refs[["pkg"]], "DESCRIPTION") 66 | 67 | f <- tempfile() 68 | on.exit(unlink(f)) 69 | 70 | utils::download.file(url, f) 71 | desc <- read_dcf(f) 72 | 73 | url_fields <- c(desc$URL, desc$BugReports) 74 | 75 | if (length(url_fields) == 0) { 76 | return(FALSE) 77 | } 78 | 79 | pkg_urls <- unlist(strsplit(url_fields, "[[:space:]]*,[[:space:]]*")) 80 | 81 | # Remove trailing "/issues" from the BugReports URL 82 | pkg_urls <- sub("/issues$", "", pkg_urls) 83 | 84 | valid_domains <- c("github[.]com", "gitlab[.]com", "bitbucket[.]org") 85 | 86 | parts <- 87 | re_match(pkg_urls, 88 | sprintf("^https?://(?%s)/(?%s)/(?%s)(?:/(?%s))?", 89 | domain = paste0(valid_domains, collapse = "|"), 90 | username = "[^/]+", 91 | repo = "[^/@#]+", 92 | subdir = "[^/@$ ]+" 93 | ) 94 | )[c("domain", "username", "repo", "subdir")] 95 | 96 | # Remove cases which don't match and duplicates 97 | 98 | parts <- unique(stats::na.omit(parts)) 99 | 100 | nrow(parts) == 1 101 | } 102 | 103 | if (!requireNamespace("desc", quietly = TRUE)) { 104 | install.packages("desc") 105 | } 106 | 107 | deps_df <- desc::desc_get_deps() 108 | deps_df <- deps_df[deps_df$type %in% c("Depends", "Imports", "LinkingTo", "Suggests"), ] 109 | 110 | packages <- sort(deps_df$package) 111 | packages <- intersect(packages, rownames(available.packages())) 112 | 113 | valid_dev_dep <- vapply(packages, has_dev_dep, logical(1)) 114 | 115 | # https://github.com/r-lib/remotes/issues/576 116 | valid_dev_dep[packages %in% c("igraph", "duckdb", "logging")] <- FALSE 117 | 118 | deps <- packages[valid_dev_dep] 119 | if (any(!valid_dev_dep)) { 120 | msg <- paste0( 121 | "Could not determine development repository for packages: ", 122 | paste(packages[!valid_dev_dep], collapse = ", ") 123 | ) 124 | writeLines(paste0("::warning::", msg)) 125 | } 126 | 127 | json <- paste0( 128 | '{"package":[', 129 | paste0('"', deps, '"', collapse = ","), 130 | ']}' 131 | ) 132 | writeLines(json) 133 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 134 | shell: Rscript {0} 135 | -------------------------------------------------------------------------------- /.github/workflows/dep-suggests-matrix/action.R: -------------------------------------------------------------------------------- 1 | # FIXME: Dynamic lookup by parsing https://svn.r-project.org/R/tags/ 2 | get_deps <- function() { 3 | # Determine package dependencies 4 | if (!requireNamespace("desc", quietly = TRUE)) { 5 | install.packages("desc") 6 | } 7 | 8 | deps_df <- desc::desc_get_deps() 9 | deps_df_optional <- deps_df$package[deps_df$type %in% c("Suggests", "Enhances")] 10 | deps_df_hard <- deps_df$package[deps_df$type %in% c("Depends", "Imports", "LinkingTo")] 11 | deps_df_base <- unlist(tools::standard_package_names(), use.names = FALSE) 12 | 13 | packages <- sort(deps_df_optional) 14 | packages <- intersect(packages, rownames(available.packages())) 15 | 16 | # Too big to fail, or can't be avoided: 17 | off_limits <- c("testthat", "rmarkdown", "rcmdcheck", deps_df_hard, deps_df_base) 18 | off_limits_dep <- unlist(tools::package_dependencies(off_limits, recursive = TRUE, which = "strong")) 19 | setdiff(packages, c(off_limits, off_limits_dep)) 20 | } 21 | 22 | if (Sys.getenv("GITHUB_BASE_REF") != "") { 23 | print(Sys.getenv("GITHUB_BASE_REF")) 24 | system("git fetch origin ${GITHUB_BASE_REF}") 25 | # Use .. to avoid having to fetch the entire history 26 | # https://github.com/krlmlr/actions-sync/issues/45 27 | diff_cmd <- "git diff origin/${GITHUB_BASE_REF}.. -- R/ tests/ | egrep '^[+][^+]' | grep -q ::" 28 | diff_lines <- system(diff_cmd, intern = TRUE) 29 | if (length(diff_lines) > 0) { 30 | writeLines("Changes using :: in R/ or tests/:") 31 | writeLines(diff_lines) 32 | packages <- get_deps() 33 | } else { 34 | writeLines("No changes using :: found in R/ or tests/, not checking without suggested packages") 35 | packages <- character() 36 | } 37 | } else { 38 | writeLines("No GITHUB_BASE_REF, checking without suggested packages") 39 | packages <- get_deps() 40 | } 41 | 42 | if (length(packages) > 0) { 43 | json <- paste0( 44 | '{"package":[', 45 | paste0('"', packages, '"', collapse = ","), 46 | "]}" 47 | ) 48 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 49 | writeLines(json) 50 | } else { 51 | writeLines("No suggested packages found.") 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/dep-suggests-matrix/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to compute a matrix with all suggested packages" 2 | outputs: 3 | matrix: 4 | description: "Generated matrix" 5 | value: ${{ steps.set-matrix.outputs.matrix }} 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - id: set-matrix 11 | run: | 12 | Rscript ./.github/workflows/dep-suggests-matrix/action.R 13 | shell: bash 14 | -------------------------------------------------------------------------------- /.github/workflows/fledge.yaml: -------------------------------------------------------------------------------- 1 | name: fledge 2 | 3 | on: 4 | # for manual triggers 5 | workflow_dispatch: 6 | inputs: 7 | pr: 8 | description: "Create PR" 9 | required: false 10 | type: boolean 11 | default: false 12 | # daily run 13 | schedule: 14 | - cron: "30 0 * * *" 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || '' }}-${{ github.base_ref || '' }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | check_fork: 22 | runs-on: ubuntu-24.04 23 | outputs: 24 | is_forked: ${{ steps.check.outputs.is_forked }} 25 | steps: 26 | - name: Check if the repo is forked 27 | id: check 28 | env: 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | is_forked=$(gh api repos/${{ github.repository }} | jq .fork) 32 | echo "is_forked=${is_forked}" >> $GITHUB_OUTPUT 33 | shell: bash 34 | 35 | fledge: 36 | runs-on: ubuntu-24.04 37 | needs: check_fork 38 | if: needs.check_fork.outputs.is_forked == 'false' 39 | permissions: 40 | contents: write 41 | pull-requests: write 42 | actions: write 43 | env: 44 | FLEDGE_GHA_CI: true 45 | steps: 46 | - uses: actions/checkout@v4 47 | with: 48 | fetch-depth: 0 49 | fetch-tags: true 50 | 51 | - name: Configure Git identity 52 | run: | 53 | env | sort 54 | git config --local user.name "$GITHUB_ACTOR" 55 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 56 | shell: bash 57 | 58 | - name: Update apt 59 | run: | 60 | sudo apt-get update 61 | shell: bash 62 | 63 | - uses: r-lib/actions/setup-r@v2 64 | with: 65 | use-public-rspm: true 66 | 67 | - uses: r-lib/actions/setup-r-dependencies@v2 68 | env: 69 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | pak-version: devel 72 | packages: cynkra/fledge 73 | cache-version: fledge-1 74 | 75 | - name: Count rulesets 76 | # Assume that branch is protected if ruleset exists 77 | id: rulesets 78 | env: 79 | GH_TOKEN: ${{ github.token }} 80 | run: | 81 | n_rulesets=$(gh api repos/${{ github.repository }}/rulesets -q length) 82 | echo "count=${n_rulesets}" >> $GITHUB_OUTPUT 83 | shell: bash 84 | 85 | - name: Switch to branch if branch protection is enabled 86 | if: github.ref_protected == 'true' || inputs.pr == 'true' || steps.rulesets.outputs.count > 0 87 | run: | 88 | git checkout -b fledge 89 | git push -f -u origin HEAD 90 | shell: bash 91 | 92 | - name: Bump version 93 | env: 94 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 95 | run: | 96 | check_default_branch <- ("${{ github.ref_protected == 'true' || inputs.pr == 'true' || steps.rulesets.outputs.count > 0 }}" != "true") 97 | if (fledge::bump_version(which = "dev", no_change_behavior = "noop", check_default_branch = check_default_branch)) { 98 | fledge::finalize_version(push = TRUE) 99 | } 100 | shell: Rscript {0} 101 | 102 | - name: Create and merge PR if branch protection is enabled 103 | if: github.ref_protected == 'true' || inputs.pr == 'true' || steps.rulesets.outputs.count > 0 104 | env: 105 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 | run: | 107 | set -ex 108 | if [ -n "$(git diff main --numstat)" ]; then 109 | gh pr create --base main --head fledge --fill-first 110 | gh workflow run rcc -f ref=$(git rev-parse HEAD) 111 | gh pr merge --squash --auto 112 | else 113 | echo "No changes." 114 | fi 115 | shell: bash 116 | 117 | - name: Check release 118 | env: 119 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 120 | run: | 121 | fledge:::release_after_cran_built_binaries() 122 | shell: Rscript {0} 123 | -------------------------------------------------------------------------------- /.github/workflows/get-extra/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to determine extra packages to be installed" 2 | outputs: 3 | packages: 4 | description: "List of extra packages" 5 | value: ${{ steps.get-extra.outputs.packages }} 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Get extra packages 11 | id: get-extra 12 | run: | 13 | set -x 14 | packages=$( ( grep Config/gha/extra-packages DESCRIPTION || true ) | cut -d " " -f 2) 15 | echo packages=$packages >> $GITHUB_OUTPUT 16 | shell: bash 17 | -------------------------------------------------------------------------------- /.github/workflows/git-identity/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to set up a Git identity" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Configure Git identity 7 | run: | 8 | env | sort 9 | git config --local user.name "$GITHUB_ACTOR" 10 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 11 | shell: bash 12 | -------------------------------------------------------------------------------- /.github/workflows/install/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to run for installing R packages" 2 | inputs: 3 | token: 4 | description: GitHub token, set to secrets.GITHUB_TOKEN 5 | required: true 6 | r-version: 7 | description: Passed on to r-lib/actions/setup-r@v2 8 | required: false 9 | default: release 10 | install-r: 11 | description: Passed on to r-lib/actions/setup-r@v2 12 | required: false 13 | default: true 14 | needs: 15 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 16 | required: false 17 | default: "" 18 | packages: 19 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 20 | required: false 21 | default: deps::., any::sessioninfo 22 | extra-packages: 23 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 24 | required: false 25 | default: any::rcmdcheck 26 | cache-version: 27 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 28 | required: false 29 | default: 1 30 | 31 | runs: 32 | using: "composite" 33 | steps: 34 | - name: Set environment variables 35 | run: | 36 | echo "R_REMOTES_NO_ERRORS_FROM_WARNINGS=true" | tee -a $GITHUB_ENV 37 | echo "R_KEEP_PKG_SOURCE=yes" | tee -a $GITHUB_ENV 38 | echo "_R_CHECK_SYSTEM_CLOCK_=false" | tee -a $GITHUB_ENV 39 | echo "_R_CHECK_FUTURE_FILE_TIMESTAMPS_=false" | tee -a $GITHUB_ENV 40 | # prevent rgl issues because no X11 display is available 41 | echo "RGL_USE_NULL=true" | tee -a $GITHUB_ENV 42 | # from https://github.com/r-devel/r-dev-web/blob/main/CRAN/QA/Kurt/lib/R/Scripts/check_CRAN_incoming.R 43 | echo "_R_CHECK_CRAN_INCOMING_CHECK_FILE_URIS_=true" | tee -a $GITHUB_ENV 44 | echo "_R_CHECK_CRAN_INCOMING_NOTE_GNU_MAKE_=true" | tee -a $GITHUB_ENV 45 | echo "_R_CHECK_PACKAGE_DEPENDS_IGNORE_MISSING_ENHANCES_=true" | tee -a $GITHUB_ENV 46 | echo "_R_CHECK_CODE_CLASS_IS_STRING_=true" | tee -a $GITHUB_ENV 47 | echo "_R_CHECK_CODOC_VARIABLES_IN_USAGES_=true" | tee -a $GITHUB_ENV 48 | echo "_R_CHECK_CONNECTIONS_LEFT_OPEN_=true" | tee -a $GITHUB_ENV 49 | echo "_R_CHECK_DATALIST_=true" | tee -a $GITHUB_ENV 50 | echo "_R_CHECK_NEWS_IN_PLAIN_TEXT_=true" | tee -a $GITHUB_ENV 51 | echo "_R_CHECK_PACKAGES_USED_CRAN_INCOMING_NOTES_=true" | tee -a $GITHUB_ENV 52 | echo "_R_CHECK_RD_CONTENTS_KEYWORDS_=true" | tee -a $GITHUB_ENV 53 | echo "_R_CHECK_R_DEPENDS_=warn" | tee -a $GITHUB_ENV 54 | echo "_R_CHECK_S3_METHODS_SHOW_POSSIBLE_ISSUES_=true" | tee -a $GITHUB_ENV 55 | echo "_R_CHECK_THINGS_IN_TEMP_DIR_=true" | tee -a $GITHUB_ENV 56 | echo "_R_CHECK_UNDOC_USE_ALL_NAMES_=true" | tee -a $GITHUB_ENV 57 | echo "_R_CHECK_URLS_SHOW_301_STATUS_=true" | tee -a $GITHUB_ENV 58 | echo "_R_CXX_USE_NO_REMAP_=true" | tee -a $GITHUB_ENV 59 | # There is no way to disable recency and frequency checks when the incoming checks are run 60 | # echo "_R_CHECK_CRAN_INCOMING_=true" | tee -a $GITHUB_ENV 61 | echo "_R_CHECK_CRAN_INCOMING_SKIP_LARGE_VERSION_=true" | tee -a $GITHUB_ENV 62 | echo "_R_CHECK_FORCE_SUGGESTS_=false" | tee -a $GITHUB_ENV 63 | shell: bash 64 | 65 | - name: Set environment variables (non-Windows only) 66 | if: runner.os != 'Windows' 67 | run: | 68 | echo "_R_CHECK_BASHISMS_=true" | tee -a $GITHUB_ENV 69 | shell: bash 70 | 71 | - name: Update apt 72 | if: runner.os == 'Linux' 73 | run: | 74 | sudo apt-get update 75 | sudo apt-get install -y aspell 76 | echo "_R_CHECK_CRAN_INCOMING_USE_ASPELL_=true" | tee -a $GITHUB_ENV 77 | shell: bash 78 | 79 | - name: Remove pkg-config@0.29.2 80 | if: runner.os == 'macOS' 81 | run: | 82 | brew uninstall pkg-config@0.29.2 || true 83 | shell: bash 84 | 85 | - uses: r-lib/actions/setup-pandoc@v2 86 | 87 | - uses: r-lib/actions/setup-r@v2 88 | with: 89 | r-version: ${{ inputs.r-version }} 90 | install-r: ${{ inputs.install-r }} 91 | http-user-agent: ${{ matrix.config.http-user-agent }} 92 | use-public-rspm: true 93 | 94 | - id: get-extra 95 | run: | 96 | set -x 97 | packages=$( ( grep Config/gha/extra-packages DESCRIPTION || true ) | cut -d " " -f 2) 98 | echo packages=$packages >> $GITHUB_OUTPUT 99 | shell: bash 100 | 101 | - uses: r-lib/actions/setup-r-dependencies@v2 102 | env: 103 | GITHUB_PAT: ${{ inputs.token }} 104 | with: 105 | pak-version: stable 106 | needs: ${{ inputs.needs }} 107 | packages: ${{ inputs.packages }} 108 | extra-packages: ${{ inputs.extra-packages }} ${{ ( matrix.covr && 'covr xml2' ) || '' }} ${{ steps.get-extra.outputs.packages }} 109 | cache-version: ${{ inputs.cache-version }} 110 | 111 | - name: Add pkg.lock to .gitignore 112 | run: | 113 | set -x 114 | if ! [ -f .github/.gitignore ] || [ -z "$(grep '^/pkg.lock$' .github/.gitignore)" ]; then 115 | echo /pkg.lock >> .github/.gitignore 116 | fi 117 | shell: bash 118 | 119 | - name: Add fake qpdf and checkbashisms 120 | if: runner.os == 'Linux' 121 | run: | 122 | sudo ln -s $(which true) /usr/local/bin/qpdf 123 | sudo ln -s $(which true) /usr/local/bin/checkbashisms 124 | shell: bash 125 | 126 | - name: Install ccache 127 | uses: krlmlr/ccache-action@parallel-dir 128 | with: 129 | max-size: 10G 130 | verbose: 1 131 | save: false 132 | restore: false 133 | 134 | - name: Use ccache for compiling R code, and parallelize 135 | run: | 136 | mkdir -p ~/.R 137 | echo 'CC := ccache $(CC)' >> ~/.R/Makevars 138 | echo 'CXX := ccache $(CXX)' >> ~/.R/Makevars 139 | echo 'CXX11 := ccache $(CXX11)' >> ~/.R/Makevars 140 | echo 'CXX14 := ccache $(CXX14)' >> ~/.R/Makevars 141 | echo 'CXX17 := ccache $(CXX17)' >> ~/.R/Makevars 142 | echo 'MAKEFLAGS = -j2' >> ~/.R/Makevars 143 | cat ~/.R/Makevars 144 | 145 | echo 'CCACHE_SLOPPINESS=locale,time_macros' | tee -a $GITHUB_ENV 146 | 147 | # echo 'CCACHE_DEBUG=true' | tee -a $GITHUB_ENV 148 | # echo "CCACHE_DEBUGDIR=$(dirname $(pwd))/ccache-debug" | tee -a $GITHUB_ENV 149 | # mkdir -p $(dirname $(pwd))/.ccache-debug 150 | 151 | echo 'PKG_BUILD_EXTRA_FLAGS=false' | tee -a $GITHUB_ENV 152 | 153 | # Repair 154 | git rm -rf .ccache || true 155 | rm -rf .ccache 156 | shell: bash 157 | 158 | - name: Show R CMD config --all 159 | run: | 160 | R CMD config --all 161 | shell: bash 162 | -------------------------------------------------------------------------------- /.github/workflows/lock.yaml: -------------------------------------------------------------------------------- 1 | name: "Lock threads" 2 | permissions: 3 | issues: write 4 | pull-requests: write 5 | discussions: write 6 | on: 7 | workflow_dispatch: 8 | schedule: 9 | - cron: "37 2 * * *" 10 | 11 | jobs: 12 | lock: 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: krlmlr/lock-threads@patch-1 16 | with: 17 | github-token: ${{ github.token }} 18 | issue-inactive-days: "365" 19 | issue-lock-reason: "" 20 | issue-comment: > 21 | This old thread has been automatically locked. If you think you have 22 | found something related to this, please open a new issue and link to this 23 | old issue if necessary. 24 | -------------------------------------------------------------------------------- /.github/workflows/matrix-check/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to check a matrix with all R and OS versions, computed with the versions-matrix action" 2 | inputs: 3 | matrix: 4 | description: "Generated matrix" 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Install json2yaml 11 | run: | 12 | sudo npm install -g json2yaml 13 | shell: bash 14 | 15 | - run: | 16 | matrix='${{ inputs.matrix }}' 17 | if [ -n "${matrix}" ]; then 18 | echo $matrix | jq . 19 | echo $matrix | json2yaml 20 | else 21 | echo "No matrix found" 22 | fi 23 | shell: bash 24 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown-build/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to build a pkgdown website" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Build site 7 | run: | 8 | pkgdown::build_site() 9 | shell: Rscript {0} 10 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown-deploy/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to deploy a pkgdown website" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Deploy site 7 | uses: nick-fields/retry@v3 8 | with: 9 | timeout_minutes: 15 10 | max_attempts: 10 11 | command: | 12 | R -q -e 'pkgdown::deploy_to_branch(new_process = FALSE)' 13 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Also included in R-CMD-check.yaml, this workflow only listens to pushes to branches 3 | # that start with "docs*" or "cran-*" and does not need to act on pushes to the main branch. 4 | on: 5 | push: 6 | branches: 7 | - "docs*" 8 | - "cran-*" 9 | # The main branch is excluded here, it is handled by the R-CMD-check workflow. 10 | # This workflow is only for handling pushes to designated branches. 11 | workflow_dispatch: 12 | 13 | name: pkgdown 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.sha }}-${{ github.base_ref || '' }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | pkgdown: 21 | runs-on: ubuntu-24.04 22 | 23 | name: "pkgdown" 24 | 25 | # Begin custom: services 26 | # End custom: services 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - uses: ./.github/workflows/rate-limit 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - uses: ./.github/workflows/git-identity 36 | if: github.event_name == 'push' 37 | 38 | - uses: ./.github/workflows/custom/before-install 39 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 40 | 41 | - uses: ./.github/workflows/install 42 | with: 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | cache-version: pkgdown-2 45 | needs: website 46 | extra-packages: r-lib/pkgdown local::. 47 | 48 | - uses: ./.github/workflows/custom/after-install 49 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 50 | 51 | - uses: ./.github/workflows/pkgdown-build 52 | if: github.event_name != 'push' 53 | 54 | - uses: ./.github/workflows/pkgdown-deploy 55 | if: github.event_name == 'push' 56 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: [created] 4 | name: Commands 5 | jobs: 6 | document: 7 | if: startsWith(github.event.comment.body, '/document') 8 | name: document 9 | # macos is actually better here due to native binary packages 10 | runs-on: macos-latest 11 | env: 12 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: r-lib/actions/pr-fetch@v2 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | - uses: r-lib/actions/setup-r@v2 19 | - name: Configure Git identity 20 | run: | 21 | env | sort 22 | git config --local user.name "$GITHUB_ACTOR" 23 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 24 | shell: bash 25 | - name: Install dependencies 26 | run: | 27 | install.packages(c("remotes", "roxygen2"), type = "binary") 28 | remotes::install_deps(dependencies = TRUE) 29 | shell: Rscript {0} 30 | - name: Document 31 | run: | 32 | roxygen2::roxygenise() 33 | shell: Rscript {0} 34 | - name: commit 35 | run: | 36 | if [ -n "$(git status --porcelain man/ NAMESPACE)" ]; then 37 | git add man/ NAMESPACE 38 | git commit -m 'Document' 39 | fi 40 | - uses: r-lib/actions/pr-push@v2 41 | with: 42 | repo-token: ${{ secrets.GITHUB_TOKEN }} 43 | style: 44 | if: startsWith(github.event.comment.body, '/style') 45 | name: style 46 | # macos is actually better here due to native binary packages 47 | runs-on: macos-latest 48 | env: 49 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: r-lib/actions/pr-fetch@v2 53 | with: 54 | repo-token: ${{ secrets.GITHUB_TOKEN }} 55 | - uses: r-lib/actions/setup-r@v2 56 | - name: Configure Git identity 57 | run: | 58 | env | sort 59 | git config --local user.name "$GITHUB_ACTOR" 60 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 61 | shell: bash 62 | - name: Install dependencies 63 | run: | 64 | install.packages(c("styler", "roxygen2"), type = "binary") 65 | shell: Rscript {0} 66 | - name: Style 67 | run: | 68 | styler::style_pkg(strict = FALSE) 69 | shell: Rscript {0} 70 | - name: commit 71 | run: | 72 | if [ -n "$(git status --porcelain '*.R' '*.Rmd')" ]; then 73 | git add '*.R' '*.Rmd' 74 | git commit -m 'Style' 75 | fi 76 | - uses: r-lib/actions/pr-push@v2 77 | with: 78 | repo-token: ${{ secrets.GITHUB_TOKEN }} 79 | merge: 80 | if: startsWith(github.event.comment.body, '/merge') 81 | name: merge 82 | runs-on: ubuntu-22.04 83 | steps: 84 | - name: Create and merge pull request 85 | run: | 86 | set -exo pipefail 87 | PR_DETAILS=$( curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} ) 88 | echo "$PR_DETAILS" | jq . 89 | PR_BASE=$(echo "$PR_DETAILS" | jq -r .base.ref) 90 | PR_HEAD=$(echo "$PR_DETAILS" | jq -r .head.ref) 91 | PR_URL=$(curl -s -X POST --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" --data '{ "head": "'$PR_BASE'", "base": "'$PR_HEAD'", "title": "Merge back PR target branch", "body": "Target: #${{ github.event.issue.number }}" }' https://api.github.com/repos/${{ github.repository }}/pulls | jq -r .url ) 92 | echo $PR_URL 93 | # Merging here won't run CI/CD 94 | # curl -s -X PUT --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" $PR_URL/merge 95 | # A mock job just to ensure we have a successful build status 96 | finish: 97 | runs-on: ubuntu-22.04 98 | steps: 99 | - run: true 100 | -------------------------------------------------------------------------------- /.github/workflows/rate-limit/action.yml: -------------------------------------------------------------------------------- 1 | name: "Check GitHub rate limits" 2 | inputs: 3 | token: # id of input 4 | description: GitHub token, pass secrets.GITHUB_TOKEN 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Check rate limits 11 | run: | 12 | curl -s --header "authorization: Bearer ${{ inputs.token }}" https://api.github.com/rate_limit 13 | shell: bash 14 | -------------------------------------------------------------------------------- /.github/workflows/revdep.yaml: -------------------------------------------------------------------------------- 1 | # This workflow creates many jobs, run only when a branch is created 2 | on: 3 | push: 4 | branches: 5 | - "revdep*" # never run automatically on main branch 6 | 7 | name: revdep 8 | 9 | jobs: 10 | matrix: 11 | runs-on: ubuntu-22.04 12 | outputs: 13 | matrix: ${{ steps.set-matrix.outputs.matrix }} 14 | 15 | name: Collect revdeps 16 | 17 | env: 18 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 19 | RSPM: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest 20 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 21 | # prevent rgl issues because no X11 display is available 22 | RGL_USE_NULL: true 23 | # Begin custom: env vars 24 | # End custom: env vars 25 | 26 | steps: 27 | - name: Check rate limits 28 | run: | 29 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit 30 | shell: bash 31 | 32 | - uses: actions/checkout@v4 33 | 34 | # FIXME: Avoid reissuing succesful jobs 35 | # https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#list-jobs-for-a-workflow-run 36 | # https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#workflow-runs 37 | - id: set-matrix 38 | run: | 39 | package <- read.dcf("DESCRIPTION")[, "Package"][[1]] 40 | deps <- tools:::package_dependencies(package, reverse = TRUE, which = c("Depends", "Imports", "LinkingTo", "Suggests"))[[1]] 41 | json <- paste0( 42 | '{"package":[', 43 | paste0('"', deps, '"', collapse = ","), 44 | ']}' 45 | ) 46 | writeLines(json) 47 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 48 | shell: Rscript {0} 49 | 50 | check-matrix: 51 | runs-on: ubuntu-22.04 52 | needs: matrix 53 | steps: 54 | - name: Install json2yaml 55 | run: | 56 | sudo npm install -g json2yaml 57 | 58 | - name: Check matrix definition 59 | run: | 60 | matrix='${{ needs.matrix.outputs.matrix }}' 61 | echo $matrix 62 | echo $matrix | jq . 63 | echo $matrix | json2yaml 64 | 65 | R-CMD-check: 66 | needs: matrix 67 | 68 | runs-on: ubuntu-22.04 69 | 70 | name: 'revdep: ${{ matrix.package }}' 71 | 72 | # Begin custom: services 73 | # End custom: services 74 | 75 | strategy: 76 | fail-fast: false 77 | matrix: ${{fromJson(needs.matrix.outputs.matrix)}} 78 | 79 | env: 80 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 81 | RSPM: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest 82 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 83 | # prevent rgl issues because no X11 display is available 84 | RGL_USE_NULL: true 85 | # Begin custom: env vars 86 | # End custom: env vars 87 | 88 | steps: 89 | - name: Check rate limits 90 | run: | 91 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit 92 | shell: bash 93 | 94 | - uses: actions/checkout@v4 95 | 96 | # Begin custom: before install 97 | # End custom: before install 98 | 99 | - name: Use RSPM 100 | run: | 101 | mkdir -p /home/runner/work/_temp/Library 102 | echo 'local({release <- system2("lsb_release", "-sc", stdout = TRUE); options(repos=c(CRAN = paste0("https://packagemanager.rstudio.com/all/__linux__/", release, "/latest")), HTTPUserAgent = sprintf("R/%s R (%s)", getRversion(), paste(getRversion(), R.version$platform, R.version$arch, R.version$os)))}); .libPaths("/home/runner/work/_temp/Library")' | sudo tee /etc/R/Rprofile.site 103 | 104 | - name: Install remotes 105 | run: | 106 | if (!requireNamespace("curl", quietly = TRUE)) install.packages("curl") 107 | if (!requireNamespace("remotes", quietly = TRUE)) install.packages("remotes") 108 | shell: Rscript {0} 109 | 110 | - uses: r-lib/actions/setup-pandoc@v2 111 | 112 | - name: Install system dependencies 113 | if: runner.os == 'Linux' 114 | run: | 115 | sudo apt-get update -y 116 | Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "22.04")); package <- "${{ matrix.package }}"; deps <- tools::package_dependencies(package, which = "Suggests")[[1]]; lapply(c(package, deps), function(x) { writeLines(remotes::system_requirements("ubuntu", "22.04", package = x)) })' | sort | uniq > .github/deps.sh 117 | cat .github/deps.sh 118 | sudo sh < .github/deps.sh 119 | 120 | - name: Install package 121 | run: | 122 | package <- "${{ matrix.package }}" 123 | install.packages(package, dependencies = TRUE) 124 | remotes::install_cran("rcmdcheck") 125 | shell: Rscript {0} 126 | 127 | - name: Session info old 128 | run: | 129 | options(width = 100) 130 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 131 | pkgs <- installed.packages()[, "Package"] 132 | sessioninfo::session_info(pkgs, include_base = TRUE) 133 | shell: Rscript {0} 134 | 135 | # Begin custom: after install 136 | # End custom: after install 137 | 138 | - name: Check old 139 | env: 140 | _R_CHECK_CRAN_INCOMING_: false 141 | _R_CHECK_SYSTEM_CLOCK_: false 142 | _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false 143 | # Avoid downloading binary package from RSPM 144 | run: | 145 | package <- "${{ matrix.package }}" 146 | options(HTTPUserAgent = "gha") 147 | path <- download.packages(package, destdir = ".github")[, 2] 148 | print(path) 149 | 150 | dir <- file.path("revdep", package) 151 | dir.create(dir, showWarnings = FALSE, recursive = TRUE) 152 | check <- rcmdcheck::rcmdcheck(path, args = c("--no-manual", "--as-cran"), error_on = "never", check_dir = file.path(dir, "check")) 153 | file.rename(file.path(dir, "check"), file.path(dir, "old")) 154 | saveRDS(check, file.path(dir, "old.rds")) 155 | shell: Rscript {0} 156 | 157 | - name: Install local package 158 | run: | 159 | remotes::install_local(".", force = TRUE) 160 | shell: Rscript {0} 161 | 162 | - name: Session info new 163 | run: | 164 | options(width = 100) 165 | pkgs <- installed.packages()[, "Package"] 166 | sessioninfo::session_info(pkgs, include_base = TRUE) 167 | shell: Rscript {0} 168 | 169 | - name: Check new 170 | env: 171 | _R_CHECK_CRAN_INCOMING_: false 172 | _R_CHECK_SYSTEM_CLOCK_: false 173 | _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false 174 | run: | 175 | package <- "${{ matrix.package }}" 176 | path <- dir(".github", pattern = paste0("^", package), full.names = TRUE)[[1]] 177 | print(path) 178 | 179 | dir <- file.path("revdep", package) 180 | check <- rcmdcheck::rcmdcheck(path, args = c("--no-manual", "--as-cran"), error_on = "never", check_dir = file.path(dir, "check")) 181 | file.rename(file.path(dir, "check"), file.path(dir, "new")) 182 | saveRDS(check, file.path(dir, "new.rds")) 183 | shell: Rscript {0} 184 | 185 | - name: Compare 186 | run: | 187 | package <- "${{ matrix.package }}" 188 | dir <- file.path("revdep", package) 189 | old <- readRDS(file.path(dir, "old.rds")) 190 | new <- readRDS(file.path(dir, "new.rds")) 191 | compare <- rcmdcheck::compare_checks(old, new) 192 | compare 193 | cmp <- compare$cmp 194 | if (!identical(cmp[cmp$which == "old", "output"], cmp[cmp$which == "new", "output"])) { 195 | if (!requireNamespace("waldo", quietly = TRUE)) install.packages("waldo") 196 | print(waldo::compare(old, new)) 197 | 198 | stop("Check output differs.") 199 | } 200 | shell: Rscript {0} 201 | 202 | - name: Upload check results 203 | if: failure() 204 | uses: actions/upload-artifact@main 205 | with: 206 | name: ${{ matrix.package }}-results 207 | path: revdep/${{ matrix.package }} 208 | 209 | - name: Check rate limits 210 | if: always() 211 | run: | 212 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit 213 | shell: bash 214 | -------------------------------------------------------------------------------- /.github/workflows/roxygenize/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to create documentation with roxygen2" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Roxygenize 7 | run: | 8 | try(roxygen2::roxygenize()) 9 | shell: Rscript {0} 10 | -------------------------------------------------------------------------------- /.github/workflows/style/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to auto-style a package" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Check styler options 7 | id: check 8 | run: | 9 | set -x 10 | scope=$( ( grep Config/autostyle/scope DESCRIPTION || true ) | cut -d " " -f 2) 11 | strict=$( ( grep Config/autostyle/strict DESCRIPTION || true ) | cut -d " " -f 2) 12 | rmd=$( ( grep Config/autostyle/rmd DESCRIPTION || true ) | cut -d " " -f 2) 13 | echo scope=$scope >> $GITHUB_OUTPUT 14 | echo strict=$strict >> $GITHUB_OUTPUT 15 | echo rmd=$rmd >> $GITHUB_OUTPUT 16 | shell: bash 17 | 18 | - uses: actions/cache@v4 19 | if: ${{ steps.check.outputs.scope }} 20 | with: 21 | path: | 22 | ~/.cache/R/R.cache 23 | key: ${{ runner.os }}-2-${{ github.run_id }}- 24 | restore-keys: | 25 | ${{ runner.os }}-2- 26 | 27 | - name: Imprint run ID 28 | if: ${{ steps.check.outputs.scope }} 29 | run: | 30 | mkdir -p ~/.cache/R/R.cache/styler 31 | touch ~/.cache/R/R.cache/${{ github.run_id }} 32 | shell: bash 33 | 34 | - name: Show cache 35 | if: ${{ steps.check.outputs.scope }} 36 | run: | 37 | ls -l ~/.cache/R/R.cache 38 | ls -l ~/.cache/R/R.cache/styler 39 | shell: bash 40 | 41 | - name: Enable styler cache 42 | if: ${{ steps.check.outputs.scope }} 43 | run: | 44 | styler::cache_activate(verbose = TRUE) 45 | shell: Rscript {0} 46 | 47 | - name: Run styler 48 | if: ${{ steps.check.outputs.scope }} 49 | run: | 50 | strict <- as.logical("${{ steps.check.outputs.strict }}") 51 | if (is.na(strict)) { 52 | strict <- FALSE 53 | } 54 | rmd <- as.logical("${{ steps.check.outputs.rmd }}") 55 | if (is.na(rmd)) { 56 | rmd <- TRUE 57 | } 58 | styler::style_pkg( 59 | scope = "${{ steps.check.outputs.scope }}", 60 | strict = strict, 61 | filetype = c("R", "Rprofile", if (rmd) c("Rmd", "Rmarkdown", "Rnw", "Qmd")) 62 | ) 63 | shell: Rscript {0} 64 | 65 | - name: Show cache again 66 | if: ${{ steps.check.outputs.scope }} 67 | run: | 68 | ls -l ~/.cache/R/R.cache 69 | ls -l ~/.cache/R/R.cache/styler 70 | gdu -s --inodes ~/.cache/R/R.cache/styler/* || du -s --inodes ~/.cache/R/R.cache/styler/* 71 | shell: bash 72 | -------------------------------------------------------------------------------- /.github/workflows/update-snapshots/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to create pull requests for updated testthat snapshots" 2 | description: > 3 | This action will run `testthat::test_local()` for tests that seem to use snapshots, 4 | this is determined by reading and grepping the test files. 5 | If the tests are failing, snapshots are updated, and a pull request is opened. 6 | inputs: 7 | base: 8 | description: "The base branch to create the pull request against." 9 | required: false 10 | default: "main" 11 | 12 | runs: 13 | using: "composite" 14 | steps: 15 | - name: Run tests on test files that use snapshots 16 | id: run-tests 17 | run: | 18 | ## -- Run tests on test files that use snapshots -- 19 | rx <- "^test-(.*)[.][rR]$" 20 | files <- dir("tests/testthat", pattern = rx) 21 | has_snapshot <- vapply(files, function(.x) any(grepl("snapshot", readLines(file.path("tests/testthat", .x)), fixed = TRUE)), logical(1)) 22 | if (any(has_snapshot)) { 23 | patterns <- gsub(rx, "^\\1$", files[has_snapshot]) 24 | pattern <- paste0(patterns, collapse = "|") 25 | tryCatch( 26 | { 27 | result <- as.data.frame(testthat::test_local(pattern = pattern, reporter = "silent", stop_on_failure = FALSE)) 28 | print(result) 29 | failures <- result[result$failed + result$warning > 0, ] 30 | if (nrow(failures) > 0) { 31 | writeLines("Snapshot tests failed/warned.") 32 | print(failures[names(failures) != "result"]) 33 | print(failures$result) 34 | testthat::snapshot_accept() 35 | writeLines("changed=true", Sys.getenv("GITHUB_OUTPUT")) 36 | } else { 37 | writeLines("Snapshot tests ran successfully.") 38 | } 39 | }, 40 | error = print 41 | ) 42 | } else { 43 | writeLines("No snapshots found.") 44 | } 45 | shell: Rscript {0} 46 | 47 | - name: Add snapshots to Git 48 | if: ${{ steps.run-tests.outputs.changed }} 49 | run: | 50 | ## -- Add snapshots to Git -- 51 | mkdir -p tests/testthat/_snaps 52 | git add -- tests/testthat/_snaps 53 | shell: bash 54 | 55 | - name: Check changed files 56 | if: ${{ steps.run-tests.outputs.changed }} 57 | id: check-changed 58 | run: | 59 | echo "changed=$(git status --porcelain -- tests/testthat/_snaps | head -n 1)" >> $GITHUB_OUTPUT 60 | shell: bash 61 | 62 | - name: Derive branch name 63 | if: ${{ steps.check-changed.outputs.changed }} 64 | id: matrix-desc 65 | run: | 66 | config=$(echo '${{ toJSON(matrix) }}' | jq -c .) 67 | echo "text=$(echo ${config})" >> $GITHUB_OUTPUT 68 | echo "branch=$(echo ${config} | sed -r 's/[^0-9a-zA-Z]+/-/g;s/^-//;s/-$//')" >> $GITHUB_OUTPUT 69 | shell: bash 70 | 71 | - name: Create pull request 72 | if: ${{ steps.check-changed.outputs.changed }} 73 | id: cpr 74 | uses: peter-evans/create-pull-request@v6 75 | with: 76 | base: ${{ inputs.base }} 77 | branch: snapshot-${{ inputs.base }}-${{ github.job }}-${{ steps.matrix-desc.outputs.branch }} 78 | delete-branch: true 79 | title: "test: Snapshot updates for ${{ github.job }} (${{ steps.matrix-desc.outputs.text }})" 80 | body: "Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action${{ github.event.number && format(' for #{0}', github.event.number) || '' }}." 81 | add-paths: | 82 | tests/testthat/_snaps 83 | 84 | - name: Fail if pull request created 85 | if: ${{ steps.cpr.outputs.pull-request-number }} 86 | run: | 87 | false 88 | shell: bash 89 | -------------------------------------------------------------------------------- /.github/workflows/versions-matrix/action.R: -------------------------------------------------------------------------------- 1 | # Determine active versions of R to test against 2 | tags <- xml2::read_html("https://svn.r-project.org/R/tags/") 3 | 4 | bullets <- 5 | tags |> 6 | xml2::xml_find_all("//li") |> 7 | xml2::xml_text() 8 | 9 | version_bullets <- grep("^R-([0-9]+-[0-9]+-[0-9]+)/$", bullets, value = TRUE) 10 | versions <- unique(gsub("^R-([0-9]+)-([0-9]+)-[0-9]+/$", "\\1.\\2", version_bullets)) 11 | 12 | r_release <- head(sort(as.package_version(versions), decreasing = TRUE), 5) 13 | 14 | deps <- desc::desc_get_deps() 15 | r_crit <- deps$version[deps$package == "R"] 16 | if (length(r_crit) == 1) { 17 | min_r <- as.package_version(gsub("^>= ([0-9]+[.][0-9]+)(?:.*)$", "\\1", r_crit)) 18 | r_release <- r_release[r_release >= min_r] 19 | } 20 | 21 | r_versions <- c("devel", as.character(r_release)) 22 | 23 | macos <- data.frame(os = "macos-latest", r = r_versions[2:3]) 24 | windows <- data.frame(os = "windows-latest", r = r_versions[1:3]) 25 | linux_devel <- data.frame(os = "ubuntu-22.04", r = r_versions[1], `http-user-agent` = "release", check.names = FALSE) 26 | linux <- data.frame(os = "ubuntu-22.04", r = r_versions[-1]) 27 | covr <- data.frame(os = "ubuntu-22.04", r = r_versions[2], covr = "true", desc = "with covr") 28 | 29 | include_list <- list(macos, windows, linux_devel, linux, covr) 30 | 31 | if (file.exists(".github/versions-matrix.R")) { 32 | custom <- source(".github/versions-matrix.R")$value 33 | if (is.data.frame(custom)) { 34 | custom <- list(custom) 35 | } 36 | include_list <- c(include_list, custom) 37 | } 38 | 39 | print(include_list) 40 | 41 | filter <- read.dcf("DESCRIPTION")[1, ]["Config/gha/filter"] 42 | if (!is.na(filter)) { 43 | filter_expr <- parse(text = filter)[[1]] 44 | subset_fun_expr <- bquote(function(x) subset(x, .(filter_expr))) 45 | subset_fun <- eval(subset_fun_expr) 46 | include_list <- lapply(include_list, subset_fun) 47 | print(include_list) 48 | } 49 | 50 | to_json <- function(x) { 51 | if (nrow(x) == 0) return(character()) 52 | parallel <- vector("list", length(x)) 53 | for (i in seq_along(x)) { 54 | parallel[[i]] <- paste0('"', names(x)[[i]], '":"', x[[i]], '"') 55 | } 56 | paste0("{", do.call(paste, c(parallel, sep = ",")), "}") 57 | } 58 | 59 | configs <- unlist(lapply(include_list, to_json)) 60 | json <- paste0('{"include":[', paste(configs, collapse = ","), "]}") 61 | 62 | if (Sys.getenv("GITHUB_OUTPUT") != "") { 63 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 64 | } 65 | writeLines(json) 66 | -------------------------------------------------------------------------------- /.github/workflows/versions-matrix/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to compute a matrix with all R and OS versions" 2 | 3 | outputs: 4 | matrix: 5 | description: "Generated matrix" 6 | value: ${{ steps.set-matrix.outputs.matrix }} 7 | 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Install json2yaml 12 | run: | 13 | sudo npm install -g json2yaml 14 | shell: bash 15 | 16 | - id: set-matrix 17 | run: | 18 | Rscript ./.github/workflows/versions-matrix/action.R 19 | shell: bash 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | inst/doc 5 | /vignettes/*.html 6 | /revdep/.cache.rds 7 | .projectile 8 | /docs 9 | CRAN-RELEASE 10 | CRAN-SUBMISSION 11 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: rprojroot 2 | Title: Finding Files in Project Subdirectories 3 | Version: 2.0.4.9010 4 | Authors@R: 5 | person(given = "Kirill", 6 | family = "M\u00fcller", 7 | role = c("aut", "cre"), 8 | email = "kirill@cynkra.com", 9 | comment = c(ORCID = "0000-0002-1416-3412")) 10 | Description: Robust, reliable and flexible paths to files below 11 | a project root. The 'root' of a project is defined as a directory that 12 | matches a certain criterion, e.g., it contains a certain regular file. 13 | License: MIT + file LICENSE 14 | URL: https://rprojroot.r-lib.org/, 15 | https://github.com/r-lib/rprojroot 16 | BugReports: https://github.com/r-lib/rprojroot/issues 17 | Depends: 18 | R (>= 3.0.0) 19 | Suggests: 20 | covr, 21 | knitr, 22 | lifecycle, 23 | rlang, 24 | rmarkdown, 25 | testthat (>= 3.2.0), 26 | withr 27 | VignetteBuilder: 28 | knitr 29 | Config/testthat/edition: 3 30 | Encoding: UTF-8 31 | Roxygen: list(markdown = TRUE, load = "source") 32 | RoxygenNote: 7.3.2.9000 33 | Config/autostyle/scope: line_breaks 34 | Config/autostyle/strict: true 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2020 2 | COPYRIGHT HOLDER: rprojroot authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 rprojroot authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method("|",root_criterion) 4 | S3method(as_root_criterion,character) 5 | S3method(as_root_criterion,default) 6 | S3method(as_root_criterion,root_criterion) 7 | S3method(format,root_criterion) 8 | S3method(print,root_criterion) 9 | S3method(str,root_criteria) 10 | export(as.root_criterion) 11 | export(as_root_criterion) 12 | export(criteria) 13 | export(find_package_root_file) 14 | export(find_remake_root_file) 15 | export(find_root) 16 | export(find_root_file) 17 | export(find_rstudio_root_file) 18 | export(find_testthat_root_file) 19 | export(from_wd) 20 | export(get_root_desc) 21 | export(has_basename) 22 | export(has_dir) 23 | export(has_file) 24 | export(has_file_pattern) 25 | export(is.root_criterion) 26 | export(is_drake_project) 27 | export(is_git_root) 28 | export(is_pkgdown_project) 29 | export(is_projectile_project) 30 | export(is_quarto_project) 31 | export(is_r_package) 32 | export(is_remake_project) 33 | export(is_renv_project) 34 | export(is_root_criterion) 35 | export(is_rstudio_project) 36 | export(is_svn_root) 37 | export(is_targets_project) 38 | export(is_testthat) 39 | export(is_vcs_root) 40 | export(root_criterion) 41 | export(thisfile) 42 | export(thisfile_knit) 43 | export(thisfile_r) 44 | export(thisfile_rscript) 45 | export(thisfile_source) 46 | importFrom(utils,str) 47 | importFrom(utils,tail) 48 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # rprojroot 2.0.4.9010 (2024-12-09) 4 | 5 | ## Continuous integration 6 | 7 | - Avoid failure in fledge workflow if no changes (#144). 8 | 9 | 10 | # rprojroot 2.0.4.9009 (2024-12-08) 11 | 12 | ## Continuous integration 13 | 14 | - Fetch tags for fledge workflow to avoid unnecessary NEWS entries (#142). 15 | 16 | 17 | # rprojroot 2.0.4.9008 (2024-12-07) 18 | 19 | ## Features 20 | 21 | - Avoid warnings about invalid inputs with non-native encoding (@bastistician, #80). 22 | 23 | ## Chore 24 | 25 | - Enable auto-styling (#82). 26 | 27 | ## Continuous integration 28 | 29 | - Use larger retry count for lock-threads workflow (#140). 30 | 31 | - Ignore errors when removing pkg-config on macOS (#130). 32 | 33 | - Explicit permissions (#128). 34 | 35 | - Use styler from main branch (#126). 36 | 37 | - Need to install R on Ubuntu 24.04 (#124). 38 | 39 | - Use Ubuntu 24.04 and styler PR (#122). 40 | 41 | - Correctly detect branch protection (#117). 42 | 43 | - Use stable pak (#116). 44 | 45 | - Trigger run (#115). 46 | 47 | - Use pkgdown branch (#109). 48 | 49 | ## Documentation 50 | 51 | - Switch to `index.md` (#113). 52 | 53 | ## Uncategorized 54 | 55 | - PLACEHOLDER https://github.com/r-lib/rprojroot/pull/16 (#16). 56 | 57 | 58 | # rprojroot 2.0.4.9007 (2024-09-15) 59 | 60 | ## Continuous integration 61 | 62 | - Install via R CMD INSTALL ., not pak (#107). 63 | 64 | - ci: Install via R CMD INSTALL ., not pak 65 | 66 | - ci: Bump version of upload-artifact action 67 | 68 | 69 | # rprojroot 2.0.4.9006 (2024-08-31) 70 | 71 | ## Features 72 | 73 | - Align `is_pkgdown_project` with pkgdown \> 2.0.9. (#104, #106). 74 | 75 | - Avoid warnings on Windows network drives (#105). 76 | 77 | ## Chore 78 | 79 | - Auto-update from GitHub Actions. 80 | 81 | Run: https://github.com/r-lib/rprojroot/actions/runs/10425483146 82 | 83 | - Auto-update from GitHub Actions. 84 | 85 | Run: https://github.com/r-lib/rprojroot/actions/runs/10208520735 86 | 87 | - Auto-update from GitHub Actions. 88 | 89 | Run: https://github.com/r-lib/rprojroot/actions/runs/9728439643 90 | 91 | - Auto-update from GitHub Actions. 92 | 93 | Run: https://github.com/r-lib/rprojroot/actions/runs/9691616135 94 | 95 | ## Continuous integration 96 | 97 | - Install local package for pkgdown builds. 98 | 99 | - Improve support for protected branches with fledge. 100 | 101 | - Improve support for protected branches, without fledge. 102 | 103 | - Sync with latest developments. 104 | 105 | - Use v2 instead of master. 106 | 107 | - Inline action. 108 | 109 | - Use dev roxygen2 and decor. 110 | 111 | - Fix on Windows, tweak lock workflow. 112 | 113 | - Avoid checking bashisms on Windows. 114 | 115 | - Better commit message. 116 | 117 | - Bump versions, better default, consume custom matrix. 118 | 119 | - Recent updates. 120 | 121 | 122 | # rprojroot 2.0.4.9005 (2024-01-24) 123 | 124 | - Internal changes only. 125 | 126 | 127 | # rprojroot 2.0.4.9004 (2024-01-16) 128 | 129 | ## Documentation 130 | 131 | - Clarify `subdir` argument (#103). 132 | 133 | 134 | # rprojroot 2.0.4.9003 (2024-01-15) 135 | 136 | ## Chore 137 | 138 | - Rename `is_root()` to `is_fs_root()` to avoid confusion (#101). 139 | 140 | - Add Aviator configuration. 141 | 142 | 143 | # rprojroot 2.0.4.9002 (2024-01-03) 144 | 145 | ## Documentation 146 | 147 | - Fix typo (@salim-b, #99). 148 | 149 | 150 | # rprojroot 2.0.4.9001 (2023-11-20) 151 | 152 | ## Bug fixes 153 | 154 | - Fix example for `find_root()` (@salim-b, #98). 155 | 156 | ## Testing 157 | 158 | - Replace mockr with `testthat::local_mocked_bindings()` (@salim-b, #97). 159 | 160 | 161 | # rprojroot 2.0.4.9000 (2023-11-06) 162 | 163 | - Merge branch 'cran-2.0.4'. 164 | 165 | 166 | # rprojroot 2.0.4 (2023-11-05) 167 | 168 | ## Features 169 | 170 | - Add `is_renv_project` criterion looking for an `renv.lock` file (@gadenbuie, #86). 171 | - Add `is_quarto_project` criterion looking for a Quarto project (@olivroy, #91, #92). 172 | 173 | ## Chore 174 | 175 | - Update maintainer e-mail address. 176 | 177 | ## Testing 178 | 179 | - Wrap `::` to skip if not installed in tests (#94). 180 | 181 | 182 | # rprojroot 2.0.3 (2022-03-25) 183 | 184 | - Add `is_pkgdown_project` root criterion looking for a `_pkgdown.yml`, `_pkgdown.yaml`, `pkgdown/_pkgdown.yml` and/or `inst/_pkgdown.yml` file (#79, @salim-b). 185 | - Avoid `LazyData` in `DESCRIPTION`. 186 | 187 | 188 | # rprojroot 2.0.2 (2020-11-15) 189 | 190 | ## Features 191 | 192 | - In `find_root_file()`, if the first path component is already an absolute path, the path is returned unchanged without referring to the root. This allows using both root-relative and absolute paths in `here::here()`. Mixing root-relative and absolute paths in the same call returns an error (#59). 193 | - `find_root_file()` propagates `NA` values in path components. Using tidyverse recycling rules for path components of length different from one (#66). 194 | - `has_file()` and `has_file_pattern()` gain `fixed` argument (#75). 195 | - New `is_drake_project` criterion (#34). 196 | - Add `subdir` argument to `make_fix_file()` (#33, @BarkleyBG). 197 | - Update documentation for version control criteria (#35, @uribo). 198 | 199 | ## Breaking changes 200 | 201 | - `has_file()` and `has_dir()` now throw an error if the `filepath` argument is an absolute path (#74). 202 | - `has_basename()` replaces `has_dirname()` to avoid confusion (#63). 203 | - `as_root_criterion()` and `is_root_criterion()` replace `as.` and `is.`, respectively. The latter are soft-deprecated. 204 | - `thisfile()` and related functions are soft-deprecated, now available in the whereami package (#43). 205 | 206 | ## Bug fixes 207 | 208 | - The `is_dirname()` criterion no longer considers sibling directories (#44). 209 | 210 | ## Internal 211 | 212 | - Use testthat 3e (#70). 213 | - The backports package is no longer imported (#68). 214 | - Re-license as MIT (#50). 215 | - Move checks to GitHub Actions (#52). 216 | - Availability of suggested packages knitr and rmarkdown, and pandoc, is now checked before running the corresponding tests. 217 | 218 | 219 | # rprojroot 1.3-2 (2017-12-22) 220 | 221 | - Availability of suggested packages knitr and rmarkdown, and pandoc, is now checked before running the corresponding tests. 222 | 223 | 224 | # rprojroot 1.3-1 (2017-12-18) 225 | 226 | - Adapt to testthat 2.0.0. 227 | - New `thisfile()`, moved from kimisc (#8). 228 | - Add more examples to vignette (#26, @BarkleyBG). 229 | - Detect `.git` directories created with `git clone --separate-git-dir=...` (#24, @karldw). 230 | 231 | 232 | # rprojroot 1.2 (2017-01-15) 233 | 234 | - New root criteria 235 | - `is_projectile_project` recognize projectile projects (#21). 236 | - `has_dir()` constructs root criteria that check for existence of a directory. 237 | - `is_git_root`, `is_svn_root` and `is_vcs_root` look for a version control system root (#19). 238 | 239 | - New function 240 | - `get_root_desc()` returns the description of the criterion that applies to a given root, useful for composite criteria created with `|`. 241 | 242 | - Minor enhancements 243 | - Improve formatting of alternative criteria (#18). 244 | - If root cannot be found, the start path is shown in the error message. 245 | 246 | - Internal 247 | - The `$testfun` member of the `rprojroot` S3 class is now a list of functions instead of a function. 248 | 249 | 250 | # rprojroot 1.1 (2016-10-29) 251 | 252 | - Compatibility 253 | - Compatible with R >= 3.0.0 with the help of the `backports` package. 254 | 255 | - New root criteria 256 | - `is_remake_project` and `find_remake_root_file()` look for [remake](https://github.com/richfitz/remake) project (#17). 257 | - `is_testthat` and `find_testthat_root_file()` that looks for `tests/testthat` root (#14). 258 | - `from_wd`, useful for creating accessors to a known path (#11). 259 | 260 | - Minor enhancement 261 | - Criteria can be combined with the `|` operator (#15). 262 | 263 | - Documentation 264 | - Add package documentation with a few examples (#13). 265 | - Clarify difference between `find_file()` and `make_fix_file()` in vignette (#9). 266 | - Remove unexported functions from documentation and examples (#10). 267 | - Use `pkgdown` to create website. 268 | 269 | - Testing 270 | - Use Travis instead of wercker. Travis tests three R versions, and OS X. 271 | - Improve AppVeyor testing. 272 | 273 | 274 | # rprojroot 1.0-2 (2016-03-28) 275 | 276 | - Fix test that fails on Windows only on CRAN. 277 | 278 | 279 | # rprojroot 1.0 (2016-03-26) 280 | 281 | Initial CRAN release. 282 | 283 | - S3 class `root_criterion`: 284 | - Member functions: `find_file()` and `make_fix_file()` 285 | - `root_criterion()` 286 | - `as.root_criterion()` 287 | - `is.root_criterion()` 288 | - `has_file()` 289 | - `has_file_pattern()` 290 | - Built-in criteria: 291 | - `is_r_package` 292 | - `is_rstudio_project` 293 | 294 | - Getting started: 295 | - `find_package_root_file()` 296 | - `find_rstudio_root_file()` 297 | 298 | - Use a custom notion of a project root: 299 | - `find_root()` 300 | - `find_root_file()` 301 | 302 | - Vignette 303 | -------------------------------------------------------------------------------- /R/absolute.R: -------------------------------------------------------------------------------- 1 | # adapted from fs 2 | is_absolute_path <- function(x) { 3 | grepl("^[/\\\\~]|^[a-zA-Z]:[/\\\\]", x) 4 | } 5 | -------------------------------------------------------------------------------- /R/criterion.R: -------------------------------------------------------------------------------- 1 | make_find_root_file <- function(criterion) { 2 | force(criterion) 3 | eval(bquote(function(..., path = ".") { 4 | find_root_file(..., criterion = criterion, path = path) 5 | })) 6 | } 7 | 8 | make_fix_root_file <- function(criterion, path, subdir = NULL) { 9 | root <- find_root(criterion = criterion, path = path) 10 | if (!is.null(subdir)) { 11 | root <- file.path(root, subdir) 12 | } 13 | eval(bquote(function(...) { 14 | if (!missing(..1)) { 15 | abs <- is_absolute_path(..1) 16 | if (all(abs)) { 17 | return(path(...)) 18 | } 19 | if (any(abs)) { 20 | stop("Combination of absolute and relative paths not supported.", call. = FALSE) 21 | } 22 | } 23 | 24 | path(.(root), ...) 25 | })) 26 | } 27 | 28 | #' Is a directory the project root? 29 | #' 30 | #' Objects of the `root_criterion` class decide if a 31 | #' given directory is a project root. 32 | #' 33 | #' Construct criteria using `root_criterion` in a very general fashion 34 | #' by specifying a function with a `path` argument, and a description. 35 | #' 36 | #' @param testfun `[function|list(function)]`\cr 37 | #' A function with one parameter that returns `TRUE` 38 | #' if the directory specified by this parameter is the project root, 39 | #' and `FALSE` otherwise. Can also be a list of such functions. 40 | #' @param desc `[character]`\cr 41 | #' A textual description of the test criterion, of the same length 42 | #' as `testfun`. 43 | #' @param subdir `[character]`\cr 44 | #' If given, the criterion will also be tested in the subdirectories 45 | #' defined by this argument, in the order given. 46 | #' The first existing directory will be used as a starting point. 47 | #' This is used for the [is_testthat] criterion that needs to 48 | #' *descend* into `tests/testthat` if starting at the package root, 49 | #' but stay inside `tests/testthat` if called from a testthat test. 50 | #' 51 | #' @return 52 | #' An S3 object of class `root_criterion` with the following members: 53 | #' 54 | #' @export 55 | #' 56 | #' @examples 57 | #' root_criterion(function(path) file.exists(file.path(path, "somefile")), "has somefile") 58 | #' has_file("DESCRIPTION") 59 | #' is_r_package 60 | #' \dontrun{ 61 | #' is_r_package$find_file 62 | #' is_r_package$make_fix_file(".") 63 | #' } 64 | root_criterion <- function(testfun, desc, subdir = NULL) { 65 | testfun <- check_testfun(testfun) 66 | 67 | stopifnot(length(desc) == length(testfun)) 68 | 69 | full_desc <- paste0( 70 | desc, 71 | if (!is.null(subdir)) { 72 | paste0( 73 | " (also look in subdirectories: ", 74 | paste0("`", subdir, "`", collapse = ", "), 75 | ")" 76 | ) 77 | } 78 | ) 79 | 80 | criterion <- structure( 81 | list( 82 | #' @return 83 | #' \describe{ 84 | #' \item{`testfun`}{The `testfun` argument} 85 | testfun = testfun, 86 | #' \item{`desc`}{The `desc` argument} 87 | desc = full_desc, 88 | #' \item{`subdir`}{The `subdir` argument} 89 | subdir = subdir 90 | ), 91 | class = "root_criterion" 92 | ) 93 | 94 | #' \item{`find_file`}{A function with `...` and `path` arguments 95 | #' that returns a path relative to the root, 96 | #' as specified by this criterion. 97 | #' The optional `path` argument specifies the starting directory, 98 | #' which defaults to `"."`. 99 | #' The function forwards to [find_root_file()], 100 | #' which passes `...` directly to `file.path()` 101 | #' if the first argument is an absolute path. 102 | #' } 103 | criterion$find_file <- make_find_root_file(criterion) 104 | #' \item{`make_fix_file`}{A function with a `path` argument that 105 | #' returns a function that finds paths relative to the root. For a 106 | #' criterion `cr`, the result of `cr$make_fix_file(".")(...)` 107 | #' is identical to `cr$find_file(...)`. The function created by 108 | #' `make_fix_file()` can be saved to a variable to be more independent 109 | #' of the current working directory. 110 | #' } 111 | #' } 112 | criterion$make_fix_file <- 113 | function(path = getwd(), subdir = NULL) { 114 | make_fix_root_file(criterion, path, subdir) 115 | } 116 | 117 | criterion 118 | } 119 | 120 | check_testfun <- function(testfun) { 121 | if (is.function(testfun)) { 122 | testfun <- list(testfun) 123 | } 124 | 125 | for (f in testfun) { 126 | if (!isTRUE(all.equal(names(formals(f)), "path"))) { 127 | stop("All functions in testfun must have exactly one argument 'path'", call. = FALSE) 128 | } 129 | } 130 | 131 | testfun 132 | } 133 | -------------------------------------------------------------------------------- /R/deprecated.R: -------------------------------------------------------------------------------- 1 | #' Deprecated functions 2 | #' 3 | #' Use [as_root_criterion()] and [is_root_criterion()], respectively. 4 | #' 5 | #' @inheritParams as_root_criterion 6 | #' @inheritParams is_root_criterion 7 | #' @keywords internal 8 | #' @name deprecated 9 | #' @export 10 | as.root_criterion <- function(...) { 11 | as_root_criterion(...) 12 | } 13 | 14 | #' @export 15 | #' @rdname deprecated 16 | is.root_criterion <- function(...) { 17 | is_root_criterion(...) 18 | } 19 | -------------------------------------------------------------------------------- /R/file.R: -------------------------------------------------------------------------------- 1 | #' File paths relative to the root of a directory hierarchy 2 | #' 3 | #' `find_root_file()` is a wrapper around [find_root()] that 4 | #' appends an arbitrary number of path components to the root using 5 | #' [base::file.path()]. 6 | #' 7 | #' This function operates on the notion of relative paths. 8 | #' The `...` argument is expected to contain a path relative to the root. 9 | #' If the first path component passed to `...` is already an absolute path, 10 | #' the `criterion` and `path` arguments are ignored, 11 | #' and `...` is forwarded to [file.path()]. 12 | #' 13 | #' @param criterion `[root_criterion]`\cr 14 | #' A criterion, one of the predefined [criteria] 15 | #' or created by [root_criterion()]. 16 | #' Will be coerced using [as_root_criterion()]. 17 | #' @param path `[character(1)]`\cr 18 | #' The start directory. 19 | #' @param ... `[character]`\cr 20 | #' Further path components passed to [file.path()]. 21 | #' All arguments must be the same length or length one. 22 | #' @return The normalized path of the root as specified by the search criteria, 23 | #' with the additional path components appended. 24 | #' Throws an error if no root is found. 25 | #' 26 | #' @examples 27 | #' \dontrun{ 28 | #' find_package_root_file("tests", "testthat.R") 29 | #' has_file("DESCRIPTION", "^Package: ")$find_file 30 | #' has_file("DESCRIPTION", "^Package: ")$make_fix_file(".") 31 | #' } 32 | #' 33 | #' @seealso [find_root()] [utils::glob2rx()] [base::file.path()] 34 | #' 35 | #' @export 36 | find_root_file <- function(..., criterion, path = ".") { 37 | if (!missing(..1)) { 38 | abs <- is_absolute_path(..1) 39 | if (all(abs)) { 40 | return(path(...)) 41 | } 42 | if (any(abs)) { 43 | stop("Combination of absolute and relative paths not supported.", call. = FALSE) 44 | } 45 | } 46 | 47 | root <- find_root(criterion = criterion, path = path) 48 | path(root, ...) 49 | } 50 | -------------------------------------------------------------------------------- /R/path.R: -------------------------------------------------------------------------------- 1 | # Modeled after fs::path() 2 | path <- function(...) { 3 | dots <- list(...) 4 | 5 | if (!is.null(names(dots)) && any(names(dots) != "")) { 6 | warning("Arguments must be unnamed", call. = FALSE) 7 | } 8 | 9 | # Different recycling rules for zero-length vectors 10 | lengths <- lengths(dots) 11 | if (any(lengths == 0) && all(lengths %in% 0:1)) { 12 | return(character()) 13 | } 14 | 15 | # Side effect: check recycling rules 16 | component_df <- as.data.frame(dots, stringsAsFactors = FALSE) 17 | 18 | missing <- apply(is.na(component_df), 1, any) 19 | 20 | components <- lapply(component_df, function(x) enc2utf8(as.character(x))) 21 | 22 | out <- do.call(file.path, components) 23 | out[missing] <- NA_character_ 24 | Encoding(out) <- "UTF-8" 25 | out 26 | } 27 | -------------------------------------------------------------------------------- /R/root.R: -------------------------------------------------------------------------------- 1 | #' @rdname root_criterion 2 | #' @param x `[object]`\cr 3 | #' An object. 4 | #' @export 5 | is_root_criterion <- function(x) { 6 | inherits(x, "root_criterion") 7 | } 8 | 9 | #' @rdname root_criterion 10 | #' @export 11 | as_root_criterion <- function(x) UseMethod("as_root_criterion", x) 12 | 13 | #' @details 14 | #' The `as_root_criterion()` function accepts objects of class 15 | #' `root_criterion`, and character values; the latter will be 16 | #' converted to criteria using `has_file`. 17 | #' 18 | #' @rdname root_criterion 19 | #' @export 20 | as_root_criterion.character <- function(x) { 21 | has_file(x) 22 | } 23 | 24 | #' @rdname root_criterion 25 | #' @export 26 | as_root_criterion.root_criterion <- identity 27 | 28 | #' @export 29 | as_root_criterion.default <- function(x) { 30 | stop("Cannot coerce ", x, " to type root_criterion.", call. = FALSE) 31 | } 32 | 33 | #' @export 34 | format.root_criterion <- function(x, ...) { 35 | if (length(x$desc) > 1) { 36 | c("Root criterion: one of", paste0("- ", x$desc)) 37 | } else { 38 | paste0("Root criterion: ", x$desc) 39 | } 40 | } 41 | 42 | #' @export 43 | print.root_criterion <- function(x, ...) { 44 | cat(format(x), sep = "\n") 45 | invisible(x) 46 | } 47 | 48 | #' @export 49 | #' @rdname root_criterion 50 | #' @details Root criteria can be combined with the `|` operator. The result is a 51 | #' composite root criterion that requires either of the original criteria to 52 | #' match. 53 | #' @param y `[object]`\cr 54 | #' An object. 55 | `|.root_criterion` <- function(x, y) { 56 | stopifnot(is_root_criterion(y)) 57 | 58 | root_criterion( 59 | c(x$testfun, y$testfun), 60 | c(x$desc, y$desc) 61 | ) 62 | } 63 | 64 | #' Find the root of a directory hierarchy 65 | #' 66 | #' A \emph{root} is defined as a directory that contains a regular file 67 | #' whose name matches a given pattern and which optionally contains a given text. 68 | #' The search for a root starts at a given directory (the working directory 69 | #' by default), and proceeds up the directory hierarchy. 70 | #' 71 | #' Starting from the working directory, the `find_root()` function searches 72 | #' for the root. 73 | #' If a root is found, the `...` arguments are used to construct a path; 74 | #' thus, if no extra arguments are given, the root is returned. 75 | #' If no root is found, an error is thrown. 76 | #' 77 | #' @inheritParams find_root_file 78 | #' @return The normalized path of the root as specified by the search criterion. 79 | #' Throws an error if no root is found 80 | #' 81 | #' @examples 82 | #' \dontrun{ 83 | #' find_root(has_file_pattern( 84 | #' pattern = glob2rx("DESCRIPTION"), 85 | #' contents = "^Package: " 86 | #' )) 87 | #' } 88 | #' 89 | #' @seealso [utils::glob2rx()] [file.path()] 90 | #' 91 | #' @export 92 | find_root <- function(criterion, path = ".") { 93 | criterion <- as_root_criterion(criterion) 94 | 95 | start_path <- get_start_path(path, criterion$subdir) 96 | path <- start_path 97 | 98 | for (i in seq_len(.MAX_DEPTH)) { 99 | for (f in criterion$testfun) { 100 | if (f(path)) { 101 | return(path) 102 | } 103 | } 104 | 105 | if (is_fs_root(path)) { 106 | stop("No root directory found in ", start_path, " or its parent directories. ", 107 | paste(format(criterion), collapse = "\n"), 108 | call. = FALSE 109 | ) 110 | } 111 | 112 | path <- dirname(path) 113 | } 114 | 115 | stop("Maximum search of ", .MAX_DEPTH, " exceeded. Last path: ", path, call. = FALSE) 116 | } 117 | 118 | .MAX_DEPTH <- 100L 119 | 120 | get_start_path <- function(path, subdirs) { 121 | path <- normalizePath(path, winslash = "/", mustWork = TRUE) 122 | 123 | for (subdir in subdirs) { 124 | subdir_path <- file.path(path, subdir) 125 | if (dir.exists(subdir_path)) { 126 | return(subdir_path) 127 | } 128 | } 129 | 130 | path 131 | } 132 | 133 | # Borrowed from devtools 134 | is_fs_root <- function(path) { 135 | identical( 136 | normalizePath(path, winslash = "/", mustWork = FALSE), 137 | normalizePath(dirname(path), winslash = "/", mustWork = FALSE) 138 | ) 139 | } 140 | 141 | #' @rdname find_root 142 | #' @description `get_root_desc()` returns the description of the criterion 143 | #' for a root path. This is especially useful for composite root criteria 144 | #' created with [|.root_criterion()]. 145 | #' @export 146 | get_root_desc <- function(criterion, path) { 147 | for (i in seq_along(criterion$testfun)) { 148 | if (criterion$testfun[[i]](path)) { 149 | return(criterion$desc[[i]]) 150 | } 151 | } 152 | 153 | stop("path is not a root. ", 154 | paste(format(criterion), collapse = "\n"), 155 | call. = FALSE 156 | ) 157 | } 158 | 159 | 160 | format_lines <- function(n) { 161 | if (n == 1) "line" else paste0(n, " lines") 162 | } 163 | 164 | #' @details 165 | #' The `has_file()` function constructs a criterion that checks for the 166 | #' existence of a specific file (which itself can be in a subdirectory of the 167 | #' root) with specific contents. 168 | #' 169 | #' @rdname root_criterion 170 | #' @param filepath `[character(1)]`\cr 171 | #' File path (can contain directories). 172 | #' @param contents,fixed `[character(1)]`\cr 173 | #' If `contents` is `NULL` (the default), file contents are not checked. 174 | #' Otherwise, `contents` is a regular expression 175 | #' (if `fixed` is `FALSE`) or a search string (if `fixed` is `TRUE`), and 176 | #' file contents are checked matching lines. 177 | #' @param n `[integerish(1)]`\cr 178 | #' Maximum number of lines to read to check file contents. 179 | #' @export 180 | has_file <- function(filepath, contents = NULL, n = -1L, fixed = FALSE) { 181 | force(filepath) 182 | stopifnot(is.character(filepath), length(filepath) == 1) 183 | force(contents) 184 | if (!is.null(contents)) { 185 | stopifnot(is.character(contents), length(contents) == 1) 186 | } 187 | force(n) 188 | stopifnot(length(n) == 1) 189 | 190 | check_relative(filepath) 191 | 192 | testfun <- eval(bquote(function(path) { 193 | testfile <- file.path(path, .(filepath)) 194 | if (!file.exists(testfile)) { 195 | return(FALSE) 196 | } 197 | if (dir.exists(testfile)) { 198 | return(FALSE) 199 | } 200 | match_contents(testfile, .(contents), .(n), .(fixed)) 201 | })) 202 | 203 | desc <- paste0( 204 | 'contains a file "', filepath, '"', 205 | if (!is.null(contents)) { 206 | paste0( 207 | " with contents ", 208 | if (!fixed) "matching ", 209 | '"', contents, '"', 210 | if (n >= 0L) paste0(" in the first ", format_lines(n)) 211 | ) 212 | } 213 | ) 214 | 215 | root_criterion(testfun, desc) 216 | } 217 | 218 | #' @details 219 | #' The `has_dir()` function constructs a criterion that checks for the 220 | #' existence of a specific directory. 221 | #' 222 | #' @rdname root_criterion 223 | #' @export 224 | has_dir <- function(filepath) { 225 | force(filepath) 226 | stopifnot(is.character(filepath), length(filepath) == 1) 227 | 228 | check_relative(filepath) 229 | 230 | testfun <- eval(bquote(function(path) { 231 | testfile <- file.path(path, .(filepath)) 232 | dir.exists(testfile) 233 | })) 234 | 235 | desc <- paste0('contains a directory "', filepath, '"') 236 | 237 | root_criterion(testfun, desc) 238 | } 239 | 240 | check_relative <- function(filepath) { 241 | if (is_absolute_path(filepath)) { 242 | stop("filepath must be a file or a relative path, not an absolute path.", call. = FALSE) 243 | } 244 | } 245 | 246 | #' @details 247 | #' The `has_file_pattern()` function constructs a criterion that checks for the 248 | #' existence of a file that matches a pattern, with specific contents. 249 | #' 250 | #' @rdname root_criterion 251 | #' @param pattern `[character(1)]`\cr 252 | #' Regular expression to match the file name against. 253 | #' @export 254 | has_file_pattern <- function(pattern, contents = NULL, n = -1L, fixed = FALSE) { 255 | force(pattern) 256 | stopifnot(is.character(pattern), length(pattern) == 1) 257 | force(contents) 258 | if (!is.null(contents)) { 259 | stopifnot(is.character(contents), length(contents) == 1) 260 | } 261 | force(n) 262 | stopifnot(length(n) == 1) 263 | 264 | testfun <- eval(bquote(function(path) { 265 | files <- list_files(path, .(pattern)) 266 | for (f in files) { 267 | if (!match_contents(f, .(contents), .(n), .(fixed))) { 268 | next 269 | } 270 | return(TRUE) 271 | } 272 | return(FALSE) 273 | })) 274 | 275 | desc <- paste0( 276 | 'contains a file matching "', pattern, '"', 277 | if (!is.null(contents)) { 278 | paste0( 279 | " with contents ", 280 | if (!fixed) "matching ", 281 | '"', contents, '"', 282 | if (n >= 0L) paste0(" in the first ", format_lines(n)) 283 | ) 284 | } 285 | ) 286 | 287 | root_criterion(testfun, desc) 288 | } 289 | 290 | #' @details 291 | #' The `has_basename()` function constructs a criterion that checks if the 292 | #' [base::basename()] of the root directory has a specific name, 293 | #' with support for case-insensitive file systems. 294 | #' 295 | #' @rdname root_criterion 296 | #' @param basename `[character(1)]`\cr 297 | #' The required name of the root directory. 298 | #' @export 299 | has_basename <- function(basename, subdir = NULL) { 300 | force(basename) 301 | 302 | testfun <- eval(bquote(function(path) { 303 | # Support case insensitive file systems. 304 | tolower(basename(path)) == tolower(.(basename)) && dir.exists(file.path(dirname(path), .(basename))) 305 | })) 306 | 307 | desc <- paste0('directory name is "', basename, '"') 308 | 309 | root_criterion(testfun, desc, subdir = subdir) 310 | } 311 | 312 | #' @export 313 | is_rstudio_project <- has_file_pattern("[.]Rproj$", contents = "^Version: ", n = 1L) 314 | 315 | #' @export 316 | is_r_package <- has_file("DESCRIPTION", contents = "^Package: ") 317 | 318 | #' @export 319 | is_remake_project <- has_file("remake.yml") 320 | 321 | #' @export 322 | is_drake_project <- has_dir(".drake") 323 | 324 | #' @export 325 | is_targets_project <- has_file("_targets.R") 326 | 327 | #' @export 328 | is_pkgdown_project <- 329 | has_file("_pkgdown.yml") | 330 | has_file("_pkgdown.yaml") | 331 | has_file("pkgdown/_pkgdown.yml") | 332 | has_file("pkgdown/_pkgdown.yaml") | 333 | has_file("inst/_pkgdown.yml") | 334 | has_file("inst/_pkgdown.yaml") 335 | 336 | #' @export 337 | is_renv_project <- has_file("renv.lock", contents = '"Packages":\\s*\\{') 338 | 339 | #' @export 340 | is_projectile_project <- has_file(".projectile") 341 | 342 | #' @export 343 | is_quarto_project <- has_file("_quarto.yml") 344 | 345 | #' @export 346 | is_git_root <- has_dir(".git") | has_file(".git", contents = "^gitdir: ") 347 | 348 | #' @export 349 | is_svn_root <- has_dir(".svn") 350 | 351 | #' @export 352 | is_vcs_root <- is_git_root | is_svn_root 353 | 354 | #' @export 355 | is_testthat <- has_basename("testthat", c("tests/testthat", "testthat")) 356 | 357 | #' @export 358 | from_wd <- root_criterion(function(path) TRUE, "from current working directory") 359 | 360 | #' Prespecified criteria 361 | #' 362 | #' This is a collection of commonly used root criteria. 363 | #' 364 | #' @format NULL 365 | #' 366 | #' @export 367 | criteria <- structure( 368 | list( 369 | is_rstudio_project = is_rstudio_project, 370 | is_r_package = is_r_package, 371 | is_remake_project = is_remake_project, 372 | is_pkgdown_project = is_pkgdown_project, 373 | is_renv_project = is_renv_project, 374 | is_projectile_project = is_projectile_project, 375 | is_quarto_project = is_quarto_project, 376 | is_git_root = is_git_root, 377 | is_svn_root = is_svn_root, 378 | is_vcs_root = is_vcs_root, 379 | is_testthat = is_testthat, 380 | from_wd = from_wd 381 | ), 382 | class = "root_criteria" 383 | ) 384 | 385 | #' @export 386 | #' @importFrom utils str 387 | str.root_criteria <- function(object, ...) { 388 | str(lapply(object, format)) 389 | } 390 | 391 | #' @details 392 | #' `is_rstudio_project` looks for a file with extension `.Rproj`. 393 | #' 394 | #' @format NULL 395 | #' @rdname criteria 396 | #' @export 397 | "is_rstudio_project" 398 | 399 | #' @details 400 | #' `is_r_package` looks for a `DESCRIPTION` file. 401 | #' 402 | #' @format NULL 403 | #' @rdname criteria 404 | #' @export 405 | "is_r_package" 406 | 407 | #' @details 408 | #' `is_remake_project` looks for a `remake.yml` file. 409 | #' 410 | #' @format NULL 411 | #' @rdname criteria 412 | #' @export 413 | "is_remake_project" 414 | 415 | #' @details 416 | #' `is_drake_project` looks for a `.drake` directory. 417 | #' 418 | #' @format NULL 419 | #' @rdname criteria 420 | #' @export 421 | "is_drake_project" 422 | 423 | 424 | #' @details 425 | #' `is_targets_project` looks for a `_targets.R` file. 426 | #' 427 | #' @format NULL 428 | #' @rdname criteria 429 | #' @export 430 | "is_targets_project" 431 | 432 | #' @details 433 | #' `is_pkgdown_project` looks for a `_pkgdown.yml`, `_pkgdown.yaml`, `pkgdown/_pkgdown.yml` and/or `inst/_pkgdown.yml` file. 434 | #' 435 | #' @format NULL 436 | #' @rdname criteria 437 | #' @export 438 | "is_pkgdown_project" 439 | 440 | #' @details 441 | #' `is_renv_project` looks for an `renv.lock` file. 442 | #' 443 | #' @format NULL 444 | #' @rdname criteria 445 | #' @export 446 | "is_renv_project" 447 | 448 | #' @details 449 | #' `is_projectile_project` looks for a `.projectile` file. 450 | #' 451 | #' @format NULL 452 | #' @rdname criteria 453 | #' @export 454 | "is_projectile_project" 455 | 456 | #' @details 457 | #' `is_quarto_project` looks for a `_quarto.yml` file. 458 | #' 459 | #' @format NULL 460 | #' @rdname criteria 461 | #' @export 462 | "is_quarto_project" 463 | 464 | #' @details 465 | #' `is_git_root` looks for a `.git` directory. 466 | #' 467 | #' @format NULL 468 | #' @rdname criteria 469 | #' @export 470 | "is_git_root" 471 | 472 | #' @details 473 | #' `is_svn_root` looks for a `.svn` directory. 474 | #' 475 | #' @format NULL 476 | #' @rdname criteria 477 | #' @export 478 | "is_svn_root" 479 | 480 | #' @details 481 | #' `is_vcs_root` looks for the root of a version control 482 | #' system, currently only Git and SVN are supported. 483 | #' 484 | #' @format NULL 485 | #' @rdname criteria 486 | #' @export 487 | "is_vcs_root" 488 | 489 | #' @details 490 | #' `is_testthat` looks for the `testthat` directory, works when 491 | #' developing, testing, and checking a package. 492 | #' 493 | #' @format NULL 494 | #' @rdname criteria 495 | #' @export 496 | "is_testthat" 497 | 498 | #' @details 499 | #' `from_wd` uses the current working directory. 500 | #' 501 | #' @format NULL 502 | #' @rdname criteria 503 | #' @export 504 | "from_wd" 505 | -------------------------------------------------------------------------------- /R/rprojroot-package.R: -------------------------------------------------------------------------------- 1 | #' @details 2 | #' See the "Value" section in [root_criterion()] for documentation 3 | #' of root criterion objects, and [criteria] for useful predefined 4 | #' root criteria. 5 | #' 6 | #' @examples 7 | #' criteria 8 | #' \dontrun{ 9 | #' is_r_package$find_file("NAMESPACE") 10 | #' root_fun <- is_r_package$make_fix_file() 11 | #' root_fun("NAMESPACE") 12 | #' } 13 | "_PACKAGE" 14 | -------------------------------------------------------------------------------- /R/shortcut.R: -------------------------------------------------------------------------------- 1 | #' @rdname find_root_file 2 | #' @export 3 | find_rstudio_root_file <- is_rstudio_project$find_file 4 | 5 | #' @rdname find_root_file 6 | #' @export 7 | find_package_root_file <- is_r_package$find_file 8 | 9 | #' @rdname find_root_file 10 | #' @export 11 | find_remake_root_file <- is_remake_project$find_file 12 | 13 | #' @rdname find_root_file 14 | #' @export 15 | find_testthat_root_file <- is_testthat$find_file 16 | -------------------------------------------------------------------------------- /R/thisfile.R: -------------------------------------------------------------------------------- 1 | # nocov start 2 | #' Determines the path of the currently running script 3 | #' 4 | #' @description 5 | #' `r lifecycle::badge("soft-deprecated")` 6 | #' 7 | #' \R does not store nor export the path of the currently running 8 | #' script. This is an attempt to circumvent this limitation by applying 9 | #' heuristics (such as call stack and argument inspection) that work in many 10 | #' cases. 11 | #' **CAVEAT**: Use this function only if your workflow does not permit other 12 | #' solution: if a script needs to know its location, it should be set outside 13 | #' the context of the script if possible. 14 | #' 15 | #' @details This functions currently work only if the script was `source`d, 16 | #' processed with `knitr`, 17 | #' or run with `Rscript` or using the `--file` parameter to the 18 | #' `R` executable. For code run with `Rscript`, the exact value 19 | #' of the parameter passed to `Rscript` is returned. 20 | #' 21 | #' @section Life cycle: 22 | #' 23 | #' These functions are now available in the \pkg{whereami} package. 24 | #' 25 | #' @return The path of the currently running script, NULL if it cannot be 26 | #' determined. 27 | #' @seealso [base::source()], [utils::Rscript()], [base::getwd()] 28 | #' @references [https://stackoverflow.com/q/1815606/946850]() 29 | #' @author Kirill Müller, Hadley Wickham, Michael R. Head 30 | #' @keywords internal 31 | #' @examples 32 | #' \dontrun{ 33 | #' thisfile() 34 | #' } 35 | #' @export 36 | thisfile <- function() { 37 | lifecycle::deprecate_soft( 38 | "2.0.0", 39 | "rprojroot::thisfile()", 40 | "whereami::thisfile()" 41 | ) 42 | 43 | if (!is.null(res <- thisfile_source())) { 44 | res 45 | } else if (!is.null(res <- thisfile_r())) { 46 | res 47 | } else if (!is.null(res <- thisfile_rscript())) { 48 | res 49 | } else if (!is.null(res <- thisfile_knit())) { 50 | res 51 | } else { 52 | NULL 53 | } 54 | } 55 | 56 | #' @rdname thisfile 57 | #' @export 58 | thisfile_source <- function() { 59 | lifecycle::deprecate_soft( 60 | "2.0.0", 61 | "rprojroot::thisfile_source()", 62 | "whereami::thisfile_source()" 63 | ) 64 | 65 | for (i in -(1:sys.nframe())) { 66 | if (identical(args(sys.function(i)), args(base::source))) { 67 | return(normalizePath(sys.frame(i)$ofile)) 68 | } 69 | } 70 | 71 | NULL 72 | } 73 | 74 | #' @rdname thisfile 75 | #' @importFrom utils tail 76 | #' @export 77 | thisfile_r <- function() { 78 | lifecycle::deprecate_soft( 79 | "2.0.0", 80 | "rprojroot::thisfile_r()", 81 | "whereami::thisfile_r()" 82 | ) 83 | 84 | cmd_args <- commandArgs(trailingOnly = FALSE) 85 | if (!grepl("^R(?:|term)(?:|[.]exe)$", basename(cmd_args[[1L]]), ignore.case = TRUE)) { 86 | return(NULL) 87 | } 88 | 89 | cmd_args_trailing <- commandArgs(trailingOnly = TRUE) 90 | leading_idx <- 91 | seq.int(from = 1, length.out = length(cmd_args) - length(cmd_args_trailing)) 92 | cmd_args <- cmd_args[leading_idx] 93 | file_idx <- c(which(cmd_args == "-f") + 1, which(grepl("^--file=", cmd_args))) 94 | res <- gsub("^(?:|--file=)(.*)$", "\\1", cmd_args[file_idx]) 95 | 96 | # If multiple --file arguments are given, R uses the last one 97 | res <- tail(res[res != ""], 1) 98 | if (length(res) > 0) { 99 | return(res) 100 | } 101 | 102 | NULL 103 | } 104 | 105 | #' @rdname thisfile 106 | #' @importFrom utils tail 107 | #' @export 108 | thisfile_rscript <- function() { 109 | lifecycle::deprecate_soft( 110 | "2.0.0", 111 | "rprojroot::thisfile_rscript()", 112 | "whereami::thisfile_rscript()" 113 | ) 114 | 115 | cmd_args <- commandArgs(trailingOnly = FALSE) 116 | if (!grepl("^R(?:term|script)(?:|[.]exe)$", basename(cmd_args[[1L]]), ignore.case = TRUE)) { 117 | return(NULL) 118 | } 119 | 120 | cmd_args_trailing <- commandArgs(trailingOnly = TRUE) 121 | leading_idx <- 122 | seq.int(from = 1, length.out = length(cmd_args) - length(cmd_args_trailing)) 123 | cmd_args <- cmd_args[leading_idx] 124 | res <- gsub("^(?:--file=(.*)|.*)$", "\\1", cmd_args) 125 | 126 | # If multiple --file arguments are given, R uses the last one 127 | res <- tail(res[res != ""], 1) 128 | if (length(res) > 0) { 129 | return(res) 130 | } 131 | 132 | NULL 133 | } 134 | 135 | #' @rdname thisfile 136 | #' @export 137 | thisfile_knit <- function() { 138 | lifecycle::deprecate_soft( 139 | "2.0.0", 140 | "rprojroot::thisfile_knit()", 141 | "whereami::thisfile_knit()" 142 | ) 143 | 144 | if (requireNamespace("knitr")) { 145 | return(knitr::current_input(dir = TRUE)) 146 | } 147 | 148 | NULL 149 | } 150 | # nocov end 151 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | list_files <- function(path, filename) { 2 | files <- dir(path = path, pattern = filename, all.files = TRUE, full.names = TRUE) 3 | dirs <- dir.exists(files) 4 | files <- files[!dirs] 5 | files 6 | } 7 | 8 | match_contents <- function(f, contents, n, fixed) { 9 | if (is.null(contents)) { 10 | return(TRUE) 11 | } 12 | 13 | fc <- readLines(f, n) 14 | any(grepl(contents, fc, fixed = fixed, useBytes = TRUE)) 15 | } 16 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: 3 | github_document: 4 | html_preview: false 5 | --- 6 | 7 | 8 | 9 | ```{r, include = FALSE} 10 | knitr::opts_chunk$set( 11 | collapse = TRUE, 12 | comment = "#>", 13 | fig.path = "man/figures/README-", 14 | out.width = "100%" 15 | ) 16 | 17 | set.seed(20230702) 18 | 19 | clean_output <- function(x, options) { 20 | # Side effect 21 | usethis::use_build_ignore("index.md") 22 | 23 | x <- gsub("0x[0-9a-f]+", "0xdeadbeef", x) 24 | x <- gsub("dataframe_[0-9]*_[0-9]*", " dataframe_42_42 ", x) 25 | x <- gsub("[0-9]*\\.___row_number ASC", "42.___row_number ASC", x) 26 | 27 | index <- x 28 | index <- gsub("─", "-", index) 29 | index <- strsplit(paste(index, collapse = "\n"), "\n---\n")[[1]][[2]] 30 | writeLines(index, "index.md") 31 | 32 | x <- fansi::strip_sgr(x) 33 | x 34 | } 35 | 36 | options(cli.num_colors = 256) 37 | 38 | local({ 39 | hook_source <- knitr::knit_hooks$get("document") 40 | knitr::knit_hooks$set(document = clean_output) 41 | }) 42 | 43 | rlang::local_interactive(FALSE) 44 | ``` 45 | 46 | # [rprojroot](https://rprojroot.r-lib.org/) 47 | 48 | 49 | [![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html) 50 | [![rcc](https://github.com/r-lib/rprojroot/workflows/rcc/badge.svg)](https://github.com/r-lib/rprojroot/actions) 51 | [![CRAN_Status_Badge](https://www.r-pkg.org/badges/version/rprojroot)](https://cran.r-project.org/package=rprojroot) 52 | [![Codecov test coverage](https://codecov.io/gh/r-lib/rprojroot/branch/main/graph/badge.svg)](https://app.codecov.io/gh/r-lib/rprojroot?branch=main) 53 | 54 | 55 | This package helps accessing files relative to a *project root* to [stop the working directory insanity](https://gist.github.com/jennybc/362f52446fe1ebc4c49f). 56 | It is a low-level helper package for the [here](https://here.r-lib.org/) package. 57 | 58 | ```{r} 59 | library(rprojroot) 60 | ``` 61 | 62 | 63 | ## Example 64 | 65 | The rprojroot package works best when you have a "project": all related files contained in a subdirectory that can be categorized using a strict criterion. 66 | Let's create a package for demonstration. 67 | 68 | ```{r} 69 | dir <- tempfile() 70 | pkg <- usethis::create_package(dir) 71 | ``` 72 | 73 | R packages satisfy the `is_r_package` criterion. 74 | A criterion is an object that contains a `find_file()` function. 75 | With `pkg` as working directory, the function works like `file.path()`, rooted at the working directory: 76 | 77 | ```{r} 78 | setwd(pkg) 79 | is_r_package 80 | is_r_package$find_file() 81 | is_r_package$find_file("tests", "testthat") 82 | ``` 83 | 84 | This works identically when starting from a subdirectory: 85 | 86 | ```{r} 87 | setwd(file.path(pkg, "R")) 88 | is_r_package$find_file() 89 | is_r_package$find_file("tests", "testthat") 90 | ``` 91 | 92 | There is one exception: if the first component passed to `find_file()` is already an absolute path. 93 | This allows safely applying this function to paths that may be absolute or relative: 94 | 95 | ```{r} 96 | setwd(file.path(pkg, "R")) 97 | path <- is_r_package$find_file() 98 | is_r_package$find_file(path, "tests", "testthat") 99 | ``` 100 | 101 | 102 | As long as you are sure that your working directory is somewhere inside your project, you can retrieve the project root. 103 | 104 | 105 | ## Installation and further reading 106 | 107 | Install the package from CRAN: 108 | 109 | ``` r 110 | install.package("rprojroot") 111 | ``` 112 | 113 | See the [documentation](https://rprojroot.r-lib.org/articles/rprojroot.html) for more detail. 114 | 115 | --- 116 | 117 | ## Code of Conduct 118 | 119 | Please note that the rprojroot project is released with a [Contributor Code of Conduct](https://rprojroot.r-lib.org/CODE_OF_CONDUCT.html). 120 | By contributing to this project, you agree to abide by its terms. 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # [rprojroot](https://rprojroot.r-lib.org/) 5 | 6 | 7 | 8 | [![Lifecycle: 9 | stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html) 10 | [![rcc](https://github.com/r-lib/rprojroot/workflows/rcc/badge.svg)](https://github.com/r-lib/rprojroot/actions) 11 | [![CRAN_Status_Badge](https://www.r-pkg.org/badges/version/rprojroot)](https://cran.r-project.org/package=rprojroot) 12 | [![Codecov test 13 | coverage](https://codecov.io/gh/r-lib/rprojroot/branch/main/graph/badge.svg)](https://app.codecov.io/gh/r-lib/rprojroot?branch=main) 14 | 15 | 16 | This package helps accessing files relative to a *project root* to [stop 17 | the working directory 18 | insanity](https://gist.github.com/jennybc/362f52446fe1ebc4c49f). It is a 19 | low-level helper package for the [here](https://here.r-lib.org/) 20 | package. 21 | 22 | ``` r 23 | library(rprojroot) 24 | ``` 25 | 26 | ## Example 27 | 28 | The rprojroot package works best when you have a “project”: all related 29 | files contained in a subdirectory that can be categorized using a strict 30 | criterion. Let’s create a package for demonstration. 31 | 32 | ``` r 33 | dir <- tempfile() 34 | pkg <- usethis::create_package(dir) 35 | #> ✔ Creating 36 | #> /var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c/. 37 | #> ✔ Setting active project to 38 | #> "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c". 39 | #> ✔ Creating R/. 40 | #> ✔ Writing DESCRIPTION. 41 | #> Package: file2afe66d464c 42 | #> Title: What the Package Does (One Line, Title Case) 43 | #> Version: 0.0.0.9000 44 | #> Date: 2024-10-27 45 | #> Authors@R (parsed): 46 | #> * Kirill Müller [aut, cre] () 47 | #> Description: What the package does (one paragraph). 48 | #> License: GPL-3 49 | #> URL: https://github.com/krlmlr/rprojroot, 50 | #> https://krlmlr.github.io/rprojroot 51 | #> BugReports: https://github.com/krlmlr/rprojroot/issues 52 | #> Encoding: UTF-8 53 | #> Roxygen: list(markdown = TRUE) 54 | #> RoxygenNote: 7.3.2.9000 55 | #> ✔ Writing NAMESPACE. 56 | #> ✔ Setting active project to "". 57 | ``` 58 | 59 | R packages satisfy the `is_r_package` criterion. A criterion is an 60 | object that contains a `find_file()` function. With `pkg` as working 61 | directory, the function works like `file.path()`, rooted at the working 62 | directory: 63 | 64 | ``` r 65 | setwd(pkg) 66 | is_r_package 67 | #> Root criterion: contains a file "DESCRIPTION" with contents matching "^Package: " 68 | is_r_package$find_file() 69 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c" 70 | is_r_package$find_file("tests", "testthat") 71 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c/tests/testthat" 72 | ``` 73 | 74 | This works identically when starting from a subdirectory: 75 | 76 | ``` r 77 | setwd(file.path(pkg, "R")) 78 | is_r_package$find_file() 79 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c" 80 | is_r_package$find_file("tests", "testthat") 81 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c/tests/testthat" 82 | ``` 83 | 84 | There is one exception: if the first component passed to `find_file()` 85 | is already an absolute path. This allows safely applying this function 86 | to paths that may be absolute or relative: 87 | 88 | ``` r 89 | setwd(file.path(pkg, "R")) 90 | path <- is_r_package$find_file() 91 | is_r_package$find_file(path, "tests", "testthat") 92 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c/tests/testthat" 93 | ``` 94 | 95 | As long as you are sure that your working directory is somewhere inside 96 | your project, you can retrieve the project root. 97 | 98 | ## Installation and further reading 99 | 100 | Install the package from CRAN: 101 | 102 | ``` r 103 | install.package("rprojroot") 104 | ``` 105 | 106 | See the 107 | [documentation](https://rprojroot.r-lib.org/articles/rprojroot.html) for 108 | more detail. 109 | 110 | ------------------------------------------------------------------------ 111 | 112 | ## Code of Conduct 113 | 114 | Please note that the rprojroot project is released with a [Contributor 115 | Code of Conduct](https://rprojroot.r-lib.org/CODE_OF_CONDUCT.html). By 116 | contributing to this project, you agree to abide by its terms. 117 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Steps for adopting older packages to new development standards 2 | 3 | ## `docs` branch 4 | 5 | - [x] Create from last released tag 6 | - [x] Copy `.github/workflows/pkgdown.yaml` there 7 | - [x] Fix URL in DESCRIPTION, don't forget trailing slash 8 | - [x] Tweak `_pkgdown.yml` 9 | - [x] Set source of GitHub Pages to branch 10 | 11 | ## Separate pull requests 12 | 13 | - [x] Use GitHub Actions 14 | - [x] Disable tic 15 | - [x] Disable Travis CI and AppVeyor 16 | - [x] `usethis::use_coverage()` 17 | - [x] Consider open pull requests 18 | - [x] Tweak README 19 | 20 | - use `downlit::readme_document` and `load_all()` if possible 21 | 22 | - [x] Update roxygen2 23 | - [x] Use `@examplesIf` where appropriate 24 | - [x] `styler::style_pkg()` 25 | - [x] Remove `Collate:` 26 | - [x] `usethis::use_lifecycle_badge()` 27 | - [x] `usethis::use_lifecycle()` 28 | - [x] Add pkgdown reference index 29 | - [x] Adapt tests to testthat 3e 30 | - [x] Ensure that dependency between source files is cycle-free, as in https://github.com/yonicd/foreman/issues/2 31 | - [x] Ensure that source files correspond to test files via `devtools::test_coverage_file()` 32 | - [x] Import rlang where appropriate 33 | - [x] Close open issues 34 | - [x] Deprecate functions that have moved somewhere else 35 | - [x] Avoid `iris` 36 | - [x] Synchronize with own downstream packages 37 | - [x] Add type to documentation of function arguments, as in `?here::here` 38 | - [x] Check `goodpractice::gp()` 39 | - [x] Switch pkgdown to dev mode "auto" 40 | - [ ] Release to CRAN 41 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://rprojroot.r-lib.org/ 2 | 3 | reference: 4 | - title: Finding the root 5 | contents: 6 | - find_root 7 | - find_root_file 8 | - title: Criteria 9 | contents: 10 | - criteria 11 | - root_criterion 12 | - title: Package documentation 13 | contents: 14 | - '`rprojroot-package`' 15 | 16 | development: 17 | mode: auto 18 | 19 | authors: 20 | Kirill Müller: 21 | href: https://krlmlr.info 22 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | rprojroot 2.0.4 2 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | # [rprojroot](https://rprojroot.r-lib.org/) 7 | 8 | 9 | [![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html) 10 | [![rcc](https://github.com/r-lib/rprojroot/workflows/rcc/badge.svg)](https://github.com/r-lib/rprojroot/actions) 11 | [![CRAN_Status_Badge](https://www.r-pkg.org/badges/version/rprojroot)](https://cran.r-project.org/package=rprojroot) 12 | [![Codecov test coverage](https://codecov.io/gh/r-lib/rprojroot/branch/main/graph/badge.svg)](https://app.codecov.io/gh/r-lib/rprojroot?branch=main) 13 | 14 | 15 | This package helps accessing files relative to a *project root* to [stop the working directory insanity](https://gist.github.com/jennybc/362f52446fe1ebc4c49f). 16 | It is a low-level helper package for the [here](https://here.r-lib.org/) package. 17 | 18 | 19 | ``` r 20 | library(rprojroot) 21 | ``` 22 | 23 | 24 | ## Example 25 | 26 | The rprojroot package works best when you have a "project": all related files contained in a subdirectory that can be categorized using a strict criterion. 27 | Let's create a package for demonstration. 28 | 29 | 30 | ``` r 31 | dir <- tempfile() 32 | pkg <- usethis::create_package(dir) 33 | #> ✔ Creating 34 | #> /var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c/. 35 | #> ✔ Setting active project to 36 | #> "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c". 37 | #> ✔ Creating R/. 38 | #> ✔ Writing DESCRIPTION. 39 | #> Package: file2afe66d464c 40 | #> Title: What the Package Does (One Line, Title Case) 41 | #> Version: 0.0.0.9000 42 | #> Date: 2024-10-27 43 | #> Authors@R (parsed): 44 | #> * Kirill Müller [aut, cre] () 45 | #> Description: What the package does (one paragraph). 46 | #> License: GPL-3 47 | #> URL: https://github.com/krlmlr/rprojroot, 48 | #> https://krlmlr.github.io/rprojroot 49 | #> BugReports: https://github.com/krlmlr/rprojroot/issues 50 | #> Encoding: UTF-8 51 | #> Roxygen: list(markdown = TRUE) 52 | #> RoxygenNote: 7.3.2.9000 53 | #> ✔ Writing NAMESPACE. 54 | #> ✔ Setting active project to "". 55 | ``` 56 | 57 | R packages satisfy the `is_r_package` criterion. 58 | A criterion is an object that contains a `find_file()` function. 59 | With `pkg` as working directory, the function works like `file.path()`, rooted at the working directory: 60 | 61 | 62 | ``` r 63 | setwd(pkg) 64 | is_r_package 65 | #> Root criterion: contains a file "DESCRIPTION" with contents matching "^Package: " 66 | is_r_package$find_file() 67 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c" 68 | is_r_package$find_file("tests", "testthat") 69 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c/tests/testthat" 70 | ``` 71 | 72 | This works identically when starting from a subdirectory: 73 | 74 | 75 | ``` r 76 | setwd(file.path(pkg, "R")) 77 | is_r_package$find_file() 78 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c" 79 | is_r_package$find_file("tests", "testthat") 80 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c/tests/testthat" 81 | ``` 82 | 83 | There is one exception: if the first component passed to `find_file()` is already an absolute path. 84 | This allows safely applying this function to paths that may be absolute or relative: 85 | 86 | 87 | ``` r 88 | setwd(file.path(pkg, "R")) 89 | path <- is_r_package$find_file() 90 | is_r_package$find_file(path, "tests", "testthat") 91 | #> [1] "/private/var/folders/dj/yhk9rkx97wn_ykqtnmk18xvc0000gn/T/RtmpA7TxJ1/file2afe66d464c/tests/testthat" 92 | ``` 93 | 94 | 95 | As long as you are sure that your working directory is somewhere inside your project, you can retrieve the project root. 96 | 97 | 98 | ## Installation and further reading 99 | 100 | Install the package from CRAN: 101 | 102 | ``` r 103 | install.package("rprojroot") 104 | ``` 105 | 106 | See the [documentation](https://rprojroot.r-lib.org/articles/rprojroot.html) for more detail. 107 | 108 | -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | Acknowledgement 2 | AppVeyor 3 | Codecov 4 | Hadley 5 | Lifecycle 6 | ORCID 7 | RStudio 8 | Wickham 9 | backports 10 | kimisc 11 | knitr 12 | pandoc 13 | rcc 14 | readxl 15 | rmarkdown 16 | testthat 17 | tidyverse 18 | wercker 19 | whereami 20 | -------------------------------------------------------------------------------- /man/criteria.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/root.R 3 | \docType{data} 4 | \name{criteria} 5 | \alias{criteria} 6 | \alias{is_rstudio_project} 7 | \alias{is_r_package} 8 | \alias{is_remake_project} 9 | \alias{is_drake_project} 10 | \alias{is_targets_project} 11 | \alias{is_pkgdown_project} 12 | \alias{is_renv_project} 13 | \alias{is_projectile_project} 14 | \alias{is_quarto_project} 15 | \alias{is_git_root} 16 | \alias{is_svn_root} 17 | \alias{is_vcs_root} 18 | \alias{is_testthat} 19 | \alias{from_wd} 20 | \title{Prespecified criteria} 21 | \usage{ 22 | criteria 23 | 24 | is_rstudio_project 25 | 26 | is_r_package 27 | 28 | is_remake_project 29 | 30 | is_drake_project 31 | 32 | is_targets_project 33 | 34 | is_pkgdown_project 35 | 36 | is_renv_project 37 | 38 | is_projectile_project 39 | 40 | is_quarto_project 41 | 42 | is_git_root 43 | 44 | is_svn_root 45 | 46 | is_vcs_root 47 | 48 | is_testthat 49 | 50 | from_wd 51 | } 52 | \description{ 53 | This is a collection of commonly used root criteria. 54 | } 55 | \details{ 56 | \code{is_rstudio_project} looks for a file with extension \code{.Rproj}. 57 | 58 | \code{is_r_package} looks for a \code{DESCRIPTION} file. 59 | 60 | \code{is_remake_project} looks for a \code{remake.yml} file. 61 | 62 | \code{is_drake_project} looks for a \code{.drake} directory. 63 | 64 | \code{is_targets_project} looks for a \verb{_targets.R} file. 65 | 66 | \code{is_pkgdown_project} looks for a \verb{_pkgdown.yml}, \verb{_pkgdown.yaml}, \verb{pkgdown/_pkgdown.yml} and/or \verb{inst/_pkgdown.yml} file. 67 | 68 | \code{is_renv_project} looks for an \code{renv.lock} file. 69 | 70 | \code{is_projectile_project} looks for a \code{.projectile} file. 71 | 72 | \code{is_quarto_project} looks for a \verb{_quarto.yml} file. 73 | 74 | \code{is_git_root} looks for a \code{.git} directory. 75 | 76 | \code{is_svn_root} looks for a \code{.svn} directory. 77 | 78 | \code{is_vcs_root} looks for the root of a version control 79 | system, currently only Git and SVN are supported. 80 | 81 | \code{is_testthat} looks for the \code{testthat} directory, works when 82 | developing, testing, and checking a package. 83 | 84 | \code{from_wd} uses the current working directory. 85 | } 86 | \keyword{datasets} 87 | -------------------------------------------------------------------------------- /man/deprecated.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/deprecated.R 3 | \name{deprecated} 4 | \alias{deprecated} 5 | \alias{as.root_criterion} 6 | \alias{is.root_criterion} 7 | \title{Deprecated functions} 8 | \usage{ 9 | as.root_criterion(...) 10 | 11 | is.root_criterion(...) 12 | } 13 | \description{ 14 | Use \code{\link[=as_root_criterion]{as_root_criterion()}} and \code{\link[=is_root_criterion]{is_root_criterion()}}, respectively. 15 | } 16 | \keyword{internal} 17 | -------------------------------------------------------------------------------- /man/figures/lifecycle-archived.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclearchivedarchived -------------------------------------------------------------------------------- /man/figures/lifecycle-defunct.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycledefunctdefunct -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycledeprecateddeprecated -------------------------------------------------------------------------------- /man/figures/lifecycle-experimental.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycleexperimentalexperimental -------------------------------------------------------------------------------- /man/figures/lifecycle-maturing.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclematuringmaturing -------------------------------------------------------------------------------- /man/figures/lifecycle-questioning.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclequestioningquestioning -------------------------------------------------------------------------------- /man/figures/lifecycle-soft-deprecated.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclesoft-deprecatedsoft-deprecated -------------------------------------------------------------------------------- /man/figures/lifecycle-stable.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclestablestable -------------------------------------------------------------------------------- /man/figures/lifecycle-superseded.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclesupersededsuperseded -------------------------------------------------------------------------------- /man/find_root.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/root.R 3 | \name{find_root} 4 | \alias{find_root} 5 | \alias{get_root_desc} 6 | \title{Find the root of a directory hierarchy} 7 | \usage{ 8 | find_root(criterion, path = ".") 9 | 10 | get_root_desc(criterion, path) 11 | } 12 | \arguments{ 13 | \item{criterion}{\verb{[root_criterion]}\cr 14 | A criterion, one of the predefined \link{criteria} 15 | or created by \code{\link[=root_criterion]{root_criterion()}}. 16 | Will be coerced using \code{\link[=as_root_criterion]{as_root_criterion()}}.} 17 | 18 | \item{path}{\verb{[character(1)]}\cr 19 | The start directory.} 20 | } 21 | \value{ 22 | The normalized path of the root as specified by the search criterion. 23 | Throws an error if no root is found 24 | } 25 | \description{ 26 | A \emph{root} is defined as a directory that contains a regular file 27 | whose name matches a given pattern and which optionally contains a given text. 28 | The search for a root starts at a given directory (the working directory 29 | by default), and proceeds up the directory hierarchy. 30 | 31 | \code{get_root_desc()} returns the description of the criterion 32 | for a root path. This is especially useful for composite root criteria 33 | created with \code{\link[=|.root_criterion]{|.root_criterion()}}. 34 | } 35 | \details{ 36 | Starting from the working directory, the \code{find_root()} function searches 37 | for the root. 38 | If a root is found, the \code{...} arguments are used to construct a path; 39 | thus, if no extra arguments are given, the root is returned. 40 | If no root is found, an error is thrown. 41 | } 42 | \examples{ 43 | \dontrun{ 44 | find_root(has_file_pattern( 45 | pattern = glob2rx("DESCRIPTION"), 46 | contents = "^Package: " 47 | )) 48 | } 49 | 50 | } 51 | \seealso{ 52 | \code{\link[utils:glob2rx]{utils::glob2rx()}} \code{\link[=file.path]{file.path()}} 53 | } 54 | -------------------------------------------------------------------------------- /man/find_root_file.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/file.R, R/shortcut.R 3 | \name{find_root_file} 4 | \alias{find_root_file} 5 | \alias{find_rstudio_root_file} 6 | \alias{find_package_root_file} 7 | \alias{find_remake_root_file} 8 | \alias{find_testthat_root_file} 9 | \title{File paths relative to the root of a directory hierarchy} 10 | \usage{ 11 | find_root_file(..., criterion, path = ".") 12 | 13 | find_rstudio_root_file(..., path = ".") 14 | 15 | find_package_root_file(..., path = ".") 16 | 17 | find_remake_root_file(..., path = ".") 18 | 19 | find_testthat_root_file(..., path = ".") 20 | } 21 | \arguments{ 22 | \item{...}{\verb{[character]}\cr 23 | Further path components passed to \code{\link[=file.path]{file.path()}}. 24 | All arguments must be the same length or length one.} 25 | 26 | \item{criterion}{\verb{[root_criterion]}\cr 27 | A criterion, one of the predefined \link{criteria} 28 | or created by \code{\link[=root_criterion]{root_criterion()}}. 29 | Will be coerced using \code{\link[=as_root_criterion]{as_root_criterion()}}.} 30 | 31 | \item{path}{\verb{[character(1)]}\cr 32 | The start directory.} 33 | } 34 | \value{ 35 | The normalized path of the root as specified by the search criteria, 36 | with the additional path components appended. 37 | Throws an error if no root is found. 38 | } 39 | \description{ 40 | \code{find_root_file()} is a wrapper around \code{\link[=find_root]{find_root()}} that 41 | appends an arbitrary number of path components to the root using 42 | \code{\link[base:file.path]{base::file.path()}}. 43 | } 44 | \details{ 45 | This function operates on the notion of relative paths. 46 | The \code{...} argument is expected to contain a path relative to the root. 47 | If the first path component passed to \code{...} is already an absolute path, 48 | the \code{criterion} and \code{path} arguments are ignored, 49 | and \code{...} is forwarded to \code{\link[=file.path]{file.path()}}. 50 | } 51 | \examples{ 52 | \dontrun{ 53 | find_package_root_file("tests", "testthat.R") 54 | has_file("DESCRIPTION", "^Package: ")$find_file 55 | has_file("DESCRIPTION", "^Package: ")$make_fix_file(".") 56 | } 57 | 58 | } 59 | \seealso{ 60 | \code{\link[=find_root]{find_root()}} \code{\link[utils:glob2rx]{utils::glob2rx()}} \code{\link[base:file.path]{base::file.path()}} 61 | } 62 | -------------------------------------------------------------------------------- /man/root_criterion.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/criterion.R, R/root.R 3 | \name{root_criterion} 4 | \alias{root_criterion} 5 | \alias{is_root_criterion} 6 | \alias{as_root_criterion} 7 | \alias{as_root_criterion.character} 8 | \alias{as_root_criterion.root_criterion} 9 | \alias{|.root_criterion} 10 | \alias{has_file} 11 | \alias{has_dir} 12 | \alias{has_file_pattern} 13 | \alias{has_basename} 14 | \title{Is a directory the project root?} 15 | \usage{ 16 | root_criterion(testfun, desc, subdir = NULL) 17 | 18 | is_root_criterion(x) 19 | 20 | as_root_criterion(x) 21 | 22 | \method{as_root_criterion}{character}(x) 23 | 24 | \method{as_root_criterion}{root_criterion}(x) 25 | 26 | \method{|}{root_criterion}(x, y) 27 | 28 | has_file(filepath, contents = NULL, n = -1L, fixed = FALSE) 29 | 30 | has_dir(filepath) 31 | 32 | has_file_pattern(pattern, contents = NULL, n = -1L, fixed = FALSE) 33 | 34 | has_basename(basename, subdir = NULL) 35 | } 36 | \arguments{ 37 | \item{testfun}{\verb{[function|list(function)]}\cr 38 | A function with one parameter that returns \code{TRUE} 39 | if the directory specified by this parameter is the project root, 40 | and \code{FALSE} otherwise. Can also be a list of such functions.} 41 | 42 | \item{desc}{\verb{[character]}\cr 43 | A textual description of the test criterion, of the same length 44 | as \code{testfun}.} 45 | 46 | \item{subdir}{\verb{[character]}\cr 47 | If given, the criterion will also be tested in the subdirectories 48 | defined by this argument, in the order given. 49 | The first existing directory will be used as a starting point. 50 | This is used for the \link{is_testthat} criterion that needs to 51 | \emph{descend} into \code{tests/testthat} if starting at the package root, 52 | but stay inside \code{tests/testthat} if called from a testthat test.} 53 | 54 | \item{x}{\verb{[object]}\cr 55 | An object.} 56 | 57 | \item{y}{\verb{[object]}\cr 58 | An object.} 59 | 60 | \item{filepath}{\verb{[character(1)]}\cr 61 | File path (can contain directories).} 62 | 63 | \item{contents, fixed}{\verb{[character(1)]}\cr 64 | If \code{contents} is \code{NULL} (the default), file contents are not checked. 65 | Otherwise, \code{contents} is a regular expression 66 | (if \code{fixed} is \code{FALSE}) or a search string (if \code{fixed} is \code{TRUE}), and 67 | file contents are checked matching lines.} 68 | 69 | \item{n}{\verb{[integerish(1)]}\cr 70 | Maximum number of lines to read to check file contents.} 71 | 72 | \item{pattern}{\verb{[character(1)]}\cr 73 | Regular expression to match the file name against.} 74 | 75 | \item{basename}{\verb{[character(1)]}\cr 76 | The required name of the root directory.} 77 | } 78 | \value{ 79 | An S3 object of class \code{root_criterion} with the following members: 80 | 81 | \describe{ 82 | \item{\code{testfun}}{The \code{testfun} argument} 83 | \item{\code{desc}}{The \code{desc} argument} 84 | \item{\code{subdir}}{The \code{subdir} argument} 85 | \item{\code{find_file}}{A function with \code{...} and \code{path} arguments 86 | that returns a path relative to the root, 87 | as specified by this criterion. 88 | The optional \code{path} argument specifies the starting directory, 89 | which defaults to \code{"."}. 90 | The function forwards to \code{\link[=find_root_file]{find_root_file()}}, 91 | which passes \code{...} directly to \code{file.path()} 92 | if the first argument is an absolute path. 93 | } 94 | \item{\code{make_fix_file}}{A function with a \code{path} argument that 95 | returns a function that finds paths relative to the root. For a 96 | criterion \code{cr}, the result of \code{cr$make_fix_file(".")(...)} 97 | is identical to \code{cr$find_file(...)}. The function created by 98 | \code{make_fix_file()} can be saved to a variable to be more independent 99 | of the current working directory. 100 | } 101 | } 102 | } 103 | \description{ 104 | Objects of the \code{root_criterion} class decide if a 105 | given directory is a project root. 106 | } 107 | \details{ 108 | Construct criteria using \code{root_criterion} in a very general fashion 109 | by specifying a function with a \code{path} argument, and a description. 110 | 111 | The \code{as_root_criterion()} function accepts objects of class 112 | \code{root_criterion}, and character values; the latter will be 113 | converted to criteria using \code{has_file}. 114 | 115 | Root criteria can be combined with the \code{|} operator. The result is a 116 | composite root criterion that requires either of the original criteria to 117 | match. 118 | 119 | The \code{has_file()} function constructs a criterion that checks for the 120 | existence of a specific file (which itself can be in a subdirectory of the 121 | root) with specific contents. 122 | 123 | The \code{has_dir()} function constructs a criterion that checks for the 124 | existence of a specific directory. 125 | 126 | The \code{has_file_pattern()} function constructs a criterion that checks for the 127 | existence of a file that matches a pattern, with specific contents. 128 | 129 | The \code{has_basename()} function constructs a criterion that checks if the 130 | \code{\link[base:basename]{base::basename()}} of the root directory has a specific name, 131 | with support for case-insensitive file systems. 132 | } 133 | \examples{ 134 | root_criterion(function(path) file.exists(file.path(path, "somefile")), "has somefile") 135 | has_file("DESCRIPTION") 136 | is_r_package 137 | \dontrun{ 138 | is_r_package$find_file 139 | is_r_package$make_fix_file(".") 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /man/rprojroot-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rprojroot-package.R 3 | \docType{package} 4 | \name{rprojroot-package} 5 | \alias{rprojroot} 6 | \alias{rprojroot-package} 7 | \title{rprojroot: Finding Files in Project Subdirectories} 8 | \description{ 9 | Robust, reliable and flexible paths to files below a project root. The 'root' of a project is defined as a directory that matches a certain criterion, e.g., it contains a certain regular file. 10 | } 11 | \details{ 12 | See the "Value" section in \code{\link[=root_criterion]{root_criterion()}} for documentation 13 | of root criterion objects, and \link{criteria} for useful predefined 14 | root criteria. 15 | } 16 | \examples{ 17 | criteria 18 | \dontrun{ 19 | is_r_package$find_file("NAMESPACE") 20 | root_fun <- is_r_package$make_fix_file() 21 | root_fun("NAMESPACE") 22 | } 23 | } 24 | \seealso{ 25 | Useful links: 26 | \itemize{ 27 | \item \url{https://rprojroot.r-lib.org/} 28 | \item \url{https://github.com/r-lib/rprojroot} 29 | \item Report bugs at \url{https://github.com/r-lib/rprojroot/issues} 30 | } 31 | 32 | } 33 | \author{ 34 | \strong{Maintainer}: Kirill Müller \email{kirill@cynkra.com} (\href{https://orcid.org/0000-0002-1416-3412}{ORCID}) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /man/thisfile.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/thisfile.R 3 | \name{thisfile} 4 | \alias{thisfile} 5 | \alias{thisfile_source} 6 | \alias{thisfile_r} 7 | \alias{thisfile_rscript} 8 | \alias{thisfile_knit} 9 | \title{Determines the path of the currently running script} 10 | \usage{ 11 | thisfile() 12 | 13 | thisfile_source() 14 | 15 | thisfile_r() 16 | 17 | thisfile_rscript() 18 | 19 | thisfile_knit() 20 | } 21 | \value{ 22 | The path of the currently running script, NULL if it cannot be 23 | determined. 24 | } 25 | \description{ 26 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#soft-deprecated}{\figure{lifecycle-soft-deprecated.svg}{options: alt='[Soft-deprecated]'}}}{\strong{[Soft-deprecated]}} 27 | 28 | \R does not store nor export the path of the currently running 29 | script. This is an attempt to circumvent this limitation by applying 30 | heuristics (such as call stack and argument inspection) that work in many 31 | cases. 32 | \strong{CAVEAT}: Use this function only if your workflow does not permit other 33 | solution: if a script needs to know its location, it should be set outside 34 | the context of the script if possible. 35 | } 36 | \details{ 37 | This functions currently work only if the script was \code{source}d, 38 | processed with \code{knitr}, 39 | or run with \code{Rscript} or using the \code{--file} parameter to the 40 | \code{R} executable. For code run with \code{Rscript}, the exact value 41 | of the parameter passed to \code{Rscript} is returned. 42 | } 43 | \section{Life cycle}{ 44 | 45 | 46 | These functions are now available in the \pkg{whereami} package. 47 | } 48 | 49 | \examples{ 50 | \dontrun{ 51 | thisfile() 52 | } 53 | } 54 | \references{ 55 | \url{https://stackoverflow.com/q/1815606/946850} 56 | } 57 | \seealso{ 58 | \code{\link[base:source]{base::source()}}, \code{\link[utils:Rscript]{utils::Rscript()}}, \code{\link[base:getwd]{base::getwd()}} 59 | } 60 | \author{ 61 | Kirill Müller, Hadley Wickham, Michael R. Head 62 | } 63 | \keyword{internal} 64 | -------------------------------------------------------------------------------- /revdep/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !/.gitignore 3 | !/*.md 4 | -------------------------------------------------------------------------------- /revdep/README.md: -------------------------------------------------------------------------------- 1 | # Platform 2 | 3 | |field |value | 4 | |:--------|:----------------------------| 5 | |version |R version 3.4.3 (2017-11-30) | 6 | |os |Ubuntu 17.10 | 7 | |system |x86_64, linux-gnu | 8 | |ui |X11 | 9 | |language |en_US | 10 | |collate |en_US.UTF-8 | 11 | |tz |Europe/Busingen | 12 | |date |2017-12-18 | 13 | 14 | # Dependencies 15 | 16 | |package |old |new |Δ | 17 | |:---------|:-----|:------|:--| 18 | |rprojroot |1.2 |1.2-11 |* | 19 | |backports |1.1.2 |1.1.2 | | 20 | 21 | # Revdeps 22 | 23 | ## Broken (1) 24 | 25 | |package |version |error |warning |note | 26 | |:--------------------------------|:-------|:--------|:-------|:----| 27 | |[excerptr](problems.md#excerptr) |1.4.0 |1 __+1__ | | | 28 | 29 | ## All (21) 30 | 31 | |package |version |error |warning |note | 32 | |:----------------------------------------|:-------|:--------|:-------|:----| 33 | |DBI |0.7 | | | | 34 | |desc |1.1.1 | | | | 35 | |[document](problems.md#document) |2.2.0 |1 | | | 36 | |drake |4.4.0 | | | | 37 | |[excerptr](problems.md#excerptr) |1.4.0 |1 __+1__ | | | 38 | |[fakemake](problems.md#fakemake) |1.0.2 | | |1 | 39 | |[ggraptR](problems.md#ggraptr) |1.0 | | |1 | 40 | |googledrive |0.1.1 | | | | 41 | |googlesheets |0.2.2 | | | | 42 | |here |0.1 | | | | 43 | |learnr |0.9.1 | | | | 44 | |[readxl](problems.md#readxl) |1.0.0 | | |1 | 45 | |[repurrrsive](problems.md#repurrrsive) |0.1.0 | | |1 | 46 | |RMariaDB |1.0-4 | | | | 47 | |[rmarkdown](problems.md#rmarkdown) |1.8 | | |1 | 48 | |secret |1.0.0 | | | | 49 | |sparklyr |0.6.4 | | | | 50 | |styler |1.0.0 | | | | 51 | |[teachingApps](problems.md#teachingapps) |1.0.2 | | |2 | 52 | |[testthis](problems.md#testthis) |1.0.2 | |1 |1 | 53 | |usethis |1.1.0 | | | | 54 | 55 | -------------------------------------------------------------------------------- /revdep/problems.md: -------------------------------------------------------------------------------- 1 | # document 2 | 3 | Version: 2.2.0 4 | 5 | ## In both 6 | 7 | * checking tests ... 8 | ``` 9 | ERROR 10 | Running the tests in ‘tests/testthat.R’ failed. 11 | Last 13 lines of output: 12 | > library("document") 13 | > test_check("document") 14 | ── 1. Error: (unknown) (@test_basic.R#13) ───────────────────────────────────── 15 | R CMD check failed, read the above log and fix. 16 | 1: document(file_name, check_package = TRUE, runit = TRUE) at testthat/test_basic.R:13 17 | 2: check_package(package_directory = package_directory, working_directory = working_directory, 18 | check_as_cran = check_as_cran, debug = debug, stop_on_check_not_passing = stop_on_check_not_passing) 19 | 3: throw("R CMD check failed, read the above log and fix.") 20 | 21 | ══ testthat results ═══════════════════════════════════════════════════════════ 22 | OK: 0 SKIPPED: 0 FAILED: 1 23 | 1. Error: (unknown) (@test_basic.R#13) 24 | 25 | Error: testthat unit tests failed 26 | Execution halted 27 | ``` 28 | 29 | # excerptr 30 | 31 | Version: 1.4.0 32 | 33 | ## Newly broken 34 | 35 | * checking examples ... ERROR 36 | ``` 37 | Running examples in ‘excerptr-Ex.R’ failed 38 | The error most likely occurred in: 39 | 40 | > ### Name: excerptr 41 | > ### Title: Excerpt Structuring Comments and Set a Table of Contents. 42 | > ### Aliases: excerptr 43 | > 44 | > ### ** Examples 45 | > 46 | > root <- system.file(package = "excerptr") 47 | > test_files <- file.path(root, "excerpts", "tests", "files") 48 | > excerptr(file_name = file.path(test_files, "some_file.txt"), 49 | + output_path = tempdir(), run_pandoc = FALSE, 50 | + compile_latex = FALSE, 51 | + pandoc_formats = c("tex", "html")) 52 | cloning into '/home/muelleki/git/R/rprojroot/inst/excerpts'... 53 | Error in git2r::clone(url = "https://github.com/fvafrCU/excerpts/", branch = branch, : 54 | Error in 'git2r_clone': '/home/muelleki/git/R/rprojroot/inst/excerpts' exists and is not an empty directory 55 | Calls: excerptr ... get_excerpts -> -> -> .Call 56 | Execution halted 57 | ``` 58 | 59 | ## In both 60 | 61 | * checking tests ... 62 | ``` 63 | ERROR 64 | Running the tests in ‘tests/testthat.R’ failed. 65 | Last 13 lines of output: 66 | 4: tryCatchOne(tryCatchList(expr, names[-nh], parentenv, handlers[-nh]), names[nh], 67 | parentenv, handlers[[nh]]) 68 | 5: doTryCatch(return(expr), name, parentenv, handler) 69 | 6: tryCatchList(expr, names[-nh], parentenv, handlers[-nh]) 70 | 7: tryCatchOne(expr, names, parentenv, handlers[[1L]]) 71 | 8: value[[3L]](cond) 72 | 73 | ══ testthat results ═══════════════════════════════════════════════════════════ 74 | OK: 4 SKIPPED: 0 FAILED: 3 75 | 1. Error: md (@test-that.R#31) 76 | 2. Error: pandoc_formats (@test-that.R#31) 77 | 3. Error: pandoc_formats_list (@test-that.R#31) 78 | 79 | Error: testthat unit tests failed 80 | Execution halted 81 | ``` 82 | 83 | # fakemake 84 | 85 | Version: 1.0.2 86 | 87 | ## In both 88 | 89 | * checking Rd cross-references ... NOTE 90 | ``` 91 | Package unavailable to check Rd xrefs: ‘rcmdcheck’ 92 | ``` 93 | 94 | # ggraptR 95 | 96 | Version: 1.0 97 | 98 | ## In both 99 | 100 | * checking dependencies in R code ... NOTE 101 | ``` 102 | Namespaces in Imports field not imported from: 103 | ‘DBI’ ‘GGally’ ‘RColorBrewer’ ‘Rcpp’ ‘assertthat’ ‘backports’ 104 | ‘colorspace’ ‘colourpicker’ ‘evaluate’ ‘futile.options’ ‘gdtools’ 105 | ‘gtable’ ‘htmltools’ ‘htmlwidgets’ ‘httpuv’ ‘labeling’ ‘lambda.r’ 106 | ‘lazyeval’ ‘magrittr’ ‘miniUI’ ‘munsell’ ‘plyr’ ‘reshape’ ‘rprojroot’ 107 | ‘scales’ ‘stringi’ ‘stringr’ ‘svglite’ ‘tibble’ ‘xtable’ ‘yaml’ 108 | All declared Imports should be used. 109 | ``` 110 | 111 | # readxl 112 | 113 | Version: 1.0.0 114 | 115 | ## In both 116 | 117 | * checking installed package size ... NOTE 118 | ``` 119 | installed size is 5.7Mb 120 | sub-directories of 1Mb or more: 121 | libs 4.4Mb 122 | ``` 123 | 124 | # repurrrsive 125 | 126 | Version: 0.1.0 127 | 128 | ## In both 129 | 130 | * checking data for non-ASCII characters ... NOTE 131 | ``` 132 | Note: found 8 marked UTF-8 strings 133 | ``` 134 | 135 | # rmarkdown 136 | 137 | Version: 1.8 138 | 139 | ## In both 140 | 141 | * checking installed package size ... NOTE 142 | ``` 143 | installed size is 6.6Mb 144 | sub-directories of 1Mb or more: 145 | rmd 6.1Mb 146 | ``` 147 | 148 | # teachingApps 149 | 150 | Version: 1.0.2 151 | 152 | ## In both 153 | 154 | * checking installed package size ... NOTE 155 | ``` 156 | installed size is 5.6Mb 157 | sub-directories of 1Mb or more: 158 | apps 2.6Mb 159 | libs 2.1Mb 160 | ``` 161 | 162 | * checking dependencies in R code ... NOTE 163 | ``` 164 | Namespaces in Imports field not imported from: 165 | ‘data.table’ ‘datasets’ ‘stats’ 166 | All declared Imports should be used. 167 | ``` 168 | 169 | # testthis 170 | 171 | Version: 1.0.2 172 | 173 | ## In both 174 | 175 | * checking re-building of vignette outputs ... WARNING 176 | ``` 177 | Error in re-building vignettes: 178 | ... 179 | Quitting from lines 13-22 (testthis.Rmd) 180 | Error: processing vignette 'testthis.Rmd' failed with diagnostics: 181 | 'roxygen2' >= 5.0.0 must be installed for this functionality. 182 | Execution halted 183 | ``` 184 | 185 | * checking dependencies in R code ... NOTE 186 | ``` 187 | Namespace in Imports field not imported from: ‘rprojroot’ 188 | All declared Imports should be used. 189 | ``` 190 | 191 | -------------------------------------------------------------------------------- /rprojroot.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(rprojroot) 3 | 4 | test_check("rprojroot") 5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/root.md: -------------------------------------------------------------------------------- 1 | # Formatting 2 | 3 | Code 4 | format(is_r_package) 5 | Output 6 | [1] "Root criterion: contains a file \"DESCRIPTION\" with contents matching \"^Package: \"" 7 | 8 | --- 9 | 10 | Code 11 | is_r_package 12 | Output 13 | Root criterion: contains a file "DESCRIPTION" with contents matching "^Package: " 14 | 15 | --- 16 | 17 | Code 18 | is_vcs_root 19 | Output 20 | Root criterion: one of 21 | - contains a directory ".git" 22 | - contains a file ".git" with contents matching "^gitdir: " 23 | - contains a directory ".svn" 24 | 25 | --- 26 | 27 | Code 28 | has_file("a", contents = "foo", fixed = TRUE) 29 | Output 30 | Root criterion: contains a file "a" with contents "foo" 31 | 32 | --- 33 | 34 | Code 35 | has_file_pattern("a.*b", contents = "foo", fixed = TRUE) 36 | Output 37 | Root criterion: contains a file matching "a.*b" with contents "foo" 38 | 39 | --- 40 | 41 | Code 42 | criteria 43 | Output 44 | $is_rstudio_project 45 | Root criterion: contains a file matching "[.]Rproj$" with contents matching "^Version: " in the first line 46 | 47 | $is_r_package 48 | Root criterion: contains a file "DESCRIPTION" with contents matching "^Package: " 49 | 50 | $is_remake_project 51 | Root criterion: contains a file "remake.yml" 52 | 53 | $is_pkgdown_project 54 | Root criterion: one of 55 | - contains a file "_pkgdown.yml" 56 | - contains a file "_pkgdown.yaml" 57 | - contains a file "pkgdown/_pkgdown.yml" 58 | - contains a file "pkgdown/_pkgdown.yaml" 59 | - contains a file "inst/_pkgdown.yml" 60 | - contains a file "inst/_pkgdown.yaml" 61 | 62 | $is_renv_project 63 | Root criterion: contains a file "renv.lock" with contents matching ""Packages":\s*\{" 64 | 65 | $is_projectile_project 66 | Root criterion: contains a file ".projectile" 67 | 68 | $is_quarto_project 69 | Root criterion: contains a file "_quarto.yml" 70 | 71 | $is_git_root 72 | Root criterion: one of 73 | - contains a directory ".git" 74 | - contains a file ".git" with contents matching "^gitdir: " 75 | 76 | $is_svn_root 77 | Root criterion: contains a directory ".svn" 78 | 79 | $is_vcs_root 80 | Root criterion: one of 81 | - contains a directory ".git" 82 | - contains a file ".git" with contents matching "^gitdir: " 83 | - contains a directory ".svn" 84 | 85 | $is_testthat 86 | Root criterion: directory name is "testthat" (also look in subdirectories: `tests/testthat`, `testthat`) 87 | 88 | $from_wd 89 | Root criterion: from current working directory 90 | 91 | attr(,"class") 92 | [1] "root_criteria" 93 | 94 | --- 95 | 96 | Code 97 | str(criteria) 98 | Output 99 | List of 12 100 | $ is_rstudio_project : chr "Root criterion: contains a file matching \"[.]Rproj$\" with contents matching \"^Version: \" in the first line" 101 | $ is_r_package : chr "Root criterion: contains a file \"DESCRIPTION\" with contents matching \"^Package: \"" 102 | $ is_remake_project : chr "Root criterion: contains a file \"remake.yml\"" 103 | $ is_pkgdown_project : chr [1:7] "Root criterion: one of" "- contains a file \"_pkgdown.yml\"" "- contains a file \"_pkgdown.yaml\"" "- contains a file \"pkgdown/_pkgdown.yml\"" ... 104 | $ is_renv_project : chr "Root criterion: contains a file \"renv.lock\" with contents matching \"\"Packages\":\\s*\\{\"" 105 | $ is_projectile_project: chr "Root criterion: contains a file \".projectile\"" 106 | $ is_quarto_project : chr "Root criterion: contains a file \"_quarto.yml\"" 107 | $ is_git_root : chr [1:3] "Root criterion: one of" "- contains a directory \".git\"" "- contains a file \".git\" with contents matching \"^gitdir: \"" 108 | $ is_svn_root : chr "Root criterion: contains a directory \".svn\"" 109 | $ is_vcs_root : chr [1:4] "Root criterion: one of" "- contains a directory \".git\"" "- contains a file \".git\" with contents matching \"^gitdir: \"" "- contains a directory \".svn\"" 110 | $ is_testthat : chr "Root criterion: directory name is \"testthat\" (also look in subdirectories: `tests/testthat`, `testthat`)" 111 | $ from_wd : chr "Root criterion: from current working directory" 112 | 113 | # Combining criteria 114 | 115 | Code 116 | comb_crit 117 | Output 118 | Root criterion: one of 119 | - contains a file "DESCRIPTION" with contents matching "^Package: " 120 | - contains a file matching "[.]Rproj$" with contents matching "^Version: " in the first line 121 | 122 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/testthat.md: -------------------------------------------------------------------------------- 1 | # is_testthat 2 | 3 | Code 4 | is_testthat 5 | Output 6 | Root criterion: directory name is "testthat" (also look in subdirectories: `tests/testthat`, `testthat`) 7 | 8 | -------------------------------------------------------------------------------- /tests/testthat/hierarchy/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: hierarchy 2 | Title: What the Package Does (one line, title case) 3 | Version: 0.0-0 4 | Authors@R: as.person("Kirill Müller [aut, cre]") 5 | Description: What the package* does. 6 | Depends: R (>= 3.2.0) 7 | License: GPL-3 8 | LazyData: true 9 | Encoding: UTF-8 10 | -------------------------------------------------------------------------------- /tests/testthat/hierarchy/a/b/a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/hierarchy/a/b/a -------------------------------------------------------------------------------- /tests/testthat/hierarchy/a/b/b: -------------------------------------------------------------------------------- 1 | File b 2 | -------------------------------------------------------------------------------- /tests/testthat/hierarchy/a/b/c/d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/hierarchy/a/b/c/d -------------------------------------------------------------------------------- /tests/testthat/hierarchy/a/b/d/e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/hierarchy/a/b/d/e -------------------------------------------------------------------------------- /tests/testthat/hierarchy/a/remake.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/hierarchy/a/remake.yml -------------------------------------------------------------------------------- /tests/testthat/hierarchy/b: -------------------------------------------------------------------------------- 1 | File b in root 2 | -------------------------------------------------------------------------------- /tests/testthat/hierarchy/c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/hierarchy/c -------------------------------------------------------------------------------- /tests/testthat/hierarchy/hierarchy.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /tests/testthat/package/DESCRIPTION: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/package/DESCRIPTION -------------------------------------------------------------------------------- /tests/testthat/package/tests/testthat.R: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/package/tests/testthat.R -------------------------------------------------------------------------------- /tests/testthat/package/tests/testthat/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/package/tests/testthat/.gitignore -------------------------------------------------------------------------------- /tests/testthat/package/tests/testthat/test-something.R: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/package/tests/testthat/test-something.R -------------------------------------------------------------------------------- /tests/testthat/scripts/thisfile-cat.R: -------------------------------------------------------------------------------- 1 | cat(rprojroot::thisfile(), "\n", sep = "") 2 | -------------------------------------------------------------------------------- /tests/testthat/scripts/thisfile.R: -------------------------------------------------------------------------------- 1 | thisfile() 2 | -------------------------------------------------------------------------------- /tests/testthat/scripts/thisfile.Rmd: -------------------------------------------------------------------------------- 1 | ```{r echo=FALSE, results='markup', comment="", message=FALSE} 2 | message(normalizePath(rprojroot::thisfile_knit())) 3 | ``` 4 | -------------------------------------------------------------------------------- /tests/testthat/setup.R: -------------------------------------------------------------------------------- 1 | if (requireNamespace("rlang", quietly = TRUE)) { 2 | colon_colon <- `::` 3 | 4 | ensym <- rlang::ensym 5 | inject <- rlang::inject 6 | as_string <- rlang::as_string 7 | 8 | `::` <- function(x, y) { 9 | x_sym <- ensym(x) 10 | y_sym <- ensym(y) 11 | tryCatch( 12 | inject(colon_colon(!!x_sym, !!y_sym)), 13 | packageNotFoundError = function(e) { 14 | skip_if_not_installed(as_string(x_sym)) 15 | } 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/testthat/startup.Rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/startup.Rs -------------------------------------------------------------------------------- /tests/testthat/test-absolute.R: -------------------------------------------------------------------------------- 1 | # From fs 2 | describe("is_absolute_path", { 3 | it("detects windows absolute paths", { 4 | expect_true(is_absolute_path("c:\\")) 5 | expect_true(is_absolute_path("c:/")) 6 | expect_true(is_absolute_path("P:/")) 7 | expect_true(is_absolute_path("P:\\")) 8 | expect_true(is_absolute_path("\\\\server\\mountpoint\\")) 9 | expect_true(is_absolute_path("\\foo")) 10 | expect_true(is_absolute_path("\\foo\\bar")) 11 | }) 12 | it("detects posix absolute paths", { 13 | expect_false(is_absolute_path("")) 14 | expect_false(is_absolute_path("foo/bar")) 15 | expect_false(is_absolute_path("./foo/bar")) 16 | expect_false(is_absolute_path("../foo/bar")) 17 | 18 | expect_true(is_absolute_path("/")) 19 | expect_true(is_absolute_path("/foo")) 20 | expect_true(is_absolute_path("/foo/bar")) 21 | expect_true(is_absolute_path("~/foo/bar")) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/testthat/test-criterion.R: -------------------------------------------------------------------------------- 1 | test_that("Shortcuts", { 2 | expect_equal( 3 | make_find_root_file("testthat.R")("testthat"), 4 | normalizePath(getwd(), winslash = "/") 5 | ) 6 | 7 | R <- make_fix_root_file("testthat.R", getwd()) 8 | 9 | oldwd <- withr::local_dir("~") 10 | 11 | expect_equal( 12 | normalizePath(R("testthat"), mustWork = TRUE), 13 | normalizePath(oldwd, mustWork = TRUE) 14 | ) 15 | 16 | path <- R() 17 | expect_equal( 18 | normalizePath(R(path, "testthat"), mustWork = TRUE), 19 | normalizePath(oldwd, mustWork = TRUE) 20 | ) 21 | }) 22 | 23 | test_that("root_criterion", { 24 | expect_error( 25 | suppressWarnings(root_criterion(5, "Bogus")), 26 | "must have exactly one argument" 27 | ) 28 | expect_error(root_criterion(identity, "Bogus"), "must have exactly one argument") 29 | expect_true(is_root_criterion(root_criterion(function(path) FALSE, "Never"))) 30 | }) 31 | 32 | test_that("Absolute paths are returned", { 33 | expect_equal( 34 | find_root("testthat.R"), 35 | normalizePath(find_root("testthat.R"), winslash = "/") 36 | ) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/testthat/test-file.R: -------------------------------------------------------------------------------- 1 | test_that("has_file", { 2 | wd <- normalizePath(getwd(), winslash = "/") 3 | hierarchy <- function(n = 0L) { 4 | do.call(file.path, list(wd, "hierarchy", "a", "b", "c")[seq_len(n + 1L)]) 5 | } 6 | 7 | stop_path <- hierarchy(1L) 8 | path <- hierarchy(4L) 9 | 10 | local_mocked_bindings(is_fs_root = function(x) x == stop_path) 11 | 12 | expect_equal( 13 | find_root_file("c", criterion = "b/a", path = path), 14 | file.path(hierarchy(2L), "c") 15 | ) 16 | # Absolute paths are stripped 17 | expect_equal( 18 | find_root_file("/x", "y", criterion = "b/a", path = path), 19 | file.path("/x", "y") 20 | ) 21 | expect_identical( 22 | find_root_file("c", NA, criterion = "b/a", path = path), 23 | NA_character_ 24 | ) 25 | expect_identical( 26 | find_root_file("c", character(), criterion = "b/a", path = path), 27 | character() 28 | ) 29 | expect_error( 30 | find_root_file(letters[1:2], letters[1:3], criterion = "a", path = path) 31 | ) 32 | expect_error( 33 | find_root_file(letters[1:2], character(), criterion = "a", path = path) 34 | ) 35 | expect_error( 36 | find_root_file(c("b", "/x"), "c", criterion = "a", path = path), 37 | "absolute and relative" 38 | ) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/testthat/test-path.R: -------------------------------------------------------------------------------- 1 | # Adapted from fs 2 | describe("path", { 3 | it("returns paths UTF-8 encoded", { 4 | skip_on_os("solaris") 5 | expect_equal(Encoding(path("föö")), "UTF-8") 6 | }) 7 | 8 | it("returns paths UTF-8 encoded 2", { 9 | skip_on_os("solaris") 10 | skip_on_os("windows") 11 | expect_equal(Encoding(path("\U4F60\U597D.R")), "UTF-8") 12 | }) 13 | 14 | it("returns empty strings for empty inputs", { 15 | expect_equal(path(""), "") 16 | expect_equal(path(character()), character()) 17 | expect_equal(path("foo", character(), "bar"), character()) 18 | }) 19 | 20 | it("propagates NA strings", { 21 | expect_equal(path(NA_character_), NA_character_) 22 | expect_equal(path("foo", NA_character_), NA_character_) 23 | expect_equal(path(c("foo", "bar"), c("baz", NA_character_)), c("foo/baz", NA_character_)) 24 | }) 25 | 26 | it("does not double paths", { 27 | expect_equal(path("", "foo"), "/foo") 28 | 29 | # This could be a UNC path, so we keep the doubled path. 30 | expect_equal(path("//foo", "bar"), "//foo/bar") 31 | }) 32 | 33 | it("errors on paths which are too long", { 34 | expect_error(path(paste(rep("a", 100000), collapse = ""))) 35 | }) 36 | 37 | it("follows recycling rules", { 38 | expect_equal(path("foo", character()), character()) 39 | expect_equal(path("foo", "bar"), "foo/bar") 40 | expect_equal(path("foo", c("bar", "baz")), c("foo/bar", "foo/baz")) 41 | expect_equal(path(c("foo", "qux"), c("bar", "baz")), c("foo/bar", "qux/baz")) 42 | 43 | expect_error(path(c("foo", "qux", "foo2"), c("bar", "baz"))) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/testthat/test-root.R: -------------------------------------------------------------------------------- 1 | test_that("is_root_criterion", { 2 | expect_true(is_root_criterion(has_file("DESCRIPTION"))) 3 | expect_false(is_root_criterion("DESCRIPTION")) 4 | expect_true(is_root_criterion(as_root_criterion("DESCRIPTION"))) 5 | }) 6 | 7 | test_that("as_root_criterion", { 8 | reset_env <- function(x) { 9 | if (is.function(x)) { 10 | environment(x) <- .GlobalEnv 11 | } else if (is.list(x)) { 12 | x <- lapply(x, reset_env) 13 | } 14 | x 15 | } 16 | 17 | expect_equal( 18 | lapply(as_root_criterion("x"), reset_env), 19 | lapply(has_file("x"), reset_env) 20 | ) 21 | expect_error(as_root_criterion(5), "Cannot coerce") 22 | }) 23 | 24 | test_that("Formatting", { 25 | expect_snapshot(format(is_r_package)) 26 | expect_snapshot(is_r_package) 27 | expect_snapshot(is_vcs_root) 28 | expect_snapshot(has_file("a", contents = "foo", fixed = TRUE)) 29 | expect_snapshot(has_file_pattern("a.*b", contents = "foo", fixed = TRUE)) 30 | expect_snapshot(criteria) 31 | expect_snapshot(str(criteria)) 32 | }) 33 | 34 | test_that("Combining criteria", { 35 | comb_crit <- is_r_package | is_rstudio_project 36 | 37 | expect_true(is_root_criterion(comb_crit)) 38 | 39 | expect_snapshot(comb_crit) 40 | 41 | expect_equal( 42 | find_root(comb_crit, "hierarchy"), 43 | find_root(is_rstudio_project, "hierarchy/a") 44 | ) 45 | }) 46 | 47 | test_that("has_file", { 48 | wd <- normalizePath(getwd(), winslash = "/") 49 | hierarchy <- function(n = 0L) { 50 | do.call(file.path, list(wd, "hierarchy", "a", "b", "c")[seq_len(n + 1L)]) 51 | } 52 | 53 | stop_path <- hierarchy(1L) 54 | path <- hierarchy(4L) 55 | 56 | local_mocked_bindings(is_fs_root = function(x) x == stop_path) 57 | 58 | expect_equal(find_root("a", path = path), hierarchy(3L)) 59 | expect_equal(find_root("b", path = path), hierarchy(3L)) 60 | expect_equal(find_root("b/a", path = path), hierarchy(2L)) 61 | expect_equal(find_root("c", path = path), hierarchy(1L)) 62 | expect_equal(find_root("d", path = path), hierarchy(4L)) 63 | expect_equal(find_root(has_file("DESCRIPTION", "^Package: ", 1), path = path), hierarchy(1L)) 64 | expect_equal(find_root(has_file("DESCRIPTION", "^Package: "), path = path), hierarchy(1L)) 65 | expect_equal(find_root(has_file("DESCRIPTION", "package* does", fixed = TRUE), path = path), hierarchy(1L)) 66 | expect_error( 67 | find_root("test-root.R", path = path), 68 | "No root directory found" 69 | ) 70 | expect_error( 71 | find_root("rprojroot.Rproj", path = path), 72 | "No root directory found" 73 | ) 74 | expect_error( 75 | find_root(has_file("e", "f"), path = path), 76 | "No root directory found" 77 | ) 78 | expect_error( 79 | find_root(has_file("e", "f", 1), path = path), 80 | "No root directory found" 81 | ) 82 | expect_error(has_file("/a"), "absolute") 83 | }) 84 | 85 | test_that("has_file_pattern", { 86 | wd <- normalizePath(getwd(), winslash = "/") 87 | hierarchy <- function(n = 0L) { 88 | do.call(file.path, list(wd, "hierarchy", "a", "b", "c")[seq_len(n + 1L)]) 89 | } 90 | 91 | stop_path <- hierarchy(1L) 92 | path <- hierarchy(4L) 93 | 94 | local_mocked_bindings(is_fs_root = function(x) x == stop_path) 95 | 96 | expect_equal(find_root(has_file_pattern(glob2rx("a")), path = path), hierarchy(3L)) 97 | expect_equal(find_root(has_file_pattern(glob2rx("b")), path = path), hierarchy(3L)) 98 | expect_equal( 99 | find_root(has_file_pattern("[ab]", "File b"), path = path), 100 | hierarchy(3L) 101 | ) 102 | expect_equal( 103 | find_root(has_file_pattern("[ab]", "File b in root"), path = path), 104 | hierarchy(1L) 105 | ) 106 | expect_equal(find_root(has_file_pattern(glob2rx("c")), path = path), hierarchy(1L)) 107 | expect_equal(find_root(has_file_pattern(glob2rx("d")), path = path), hierarchy(4L)) 108 | expect_equal(find_root(has_file_pattern(glob2rx("DES*ION"), "^Package: ", 1), path = path), hierarchy(1L)) 109 | expect_equal(find_root(has_file_pattern(glob2rx("DESCRI?TION"), "^Package: "), path = path), hierarchy(1L)) 110 | expect_equal(find_root(has_file_pattern(glob2rx("D?SCRIPTI?N"), "package* does", fixed = TRUE), path = path), hierarchy(1L)) 111 | expect_error( 112 | find_root(has_file_pattern(glob2rx("test-root.R")), path = path), 113 | "No root directory found" 114 | ) 115 | expect_error( 116 | find_root(has_file_pattern(glob2rx("rprojroot.Rproj")), path = path), 117 | "No root directory found" 118 | ) 119 | expect_error( 120 | find_root(has_file_pattern(glob2rx("e"), "f"), path = path), 121 | "No root directory found" 122 | ) 123 | expect_error( 124 | find_root(has_file_pattern(glob2rx("e"), "f", 1), path = path), 125 | "No root directory found" 126 | ) 127 | }) 128 | 129 | test_that("has_dir", { 130 | wd <- normalizePath(getwd(), winslash = "/") 131 | hierarchy <- function(n = 0L) { 132 | do.call(file.path, list(wd, "hierarchy", "a", "b", "c")[seq_len(n + 1L)]) 133 | } 134 | 135 | stop_path <- hierarchy(1L) 136 | path <- hierarchy(4L) 137 | 138 | local_mocked_bindings(is_fs_root = function(x) x == stop_path) 139 | 140 | expect_equal(find_root(has_dir("a"), path = path), hierarchy(1L)) 141 | expect_equal(find_root(has_dir("b"), path = path), hierarchy(2L)) 142 | expect_equal(find_root(has_dir("c"), path = path), hierarchy(3L)) 143 | expect_error( 144 | find_root(has_dir("e"), path = path), 145 | "No root directory found" 146 | ) 147 | expect_error( 148 | find_root(has_dir("rprojroot.Rproj"), path = path), 149 | "No root directory found" 150 | ) 151 | expect_error(has_dir("/a"), "absolute") 152 | }) 153 | 154 | test_that("has_basename", { 155 | wd <- normalizePath(getwd(), winslash = "/") 156 | hierarchy <- function(n = 0L) { 157 | do.call(file.path, list(wd, "hierarchy", "a", "b", "c")[seq_len(n + 1L)]) 158 | } 159 | 160 | stop_path <- hierarchy(1L) 161 | path <- hierarchy(4L) 162 | 163 | local_mocked_bindings(is_fs_root = function(x) x == stop_path) 164 | 165 | expect_equal(find_root(has_basename("a"), path = path), hierarchy(2L)) 166 | expect_equal(find_root(has_basename("b"), path = path), hierarchy(3L)) 167 | expect_equal(find_root(has_basename("c"), path = path), hierarchy(4L)) 168 | expect_error( 169 | find_root(has_basename("d"), path = path), 170 | "No root directory found" 171 | ) 172 | expect_error( 173 | find_root(has_basename("rprojroot.Rproj"), path = path), 174 | "No root directory found" 175 | ) 176 | }) 177 | 178 | test_that("concrete criteria", { 179 | wd <- normalizePath(getwd(), winslash = "/") 180 | hierarchy <- function(n = 0L) { 181 | do.call(file.path, list(wd, "hierarchy", "a", "b", "c")[seq_len(n + 1L)]) 182 | } 183 | 184 | # HACK 185 | writeLines(character(), file.path(hierarchy(3L), ".projectile")) 186 | 187 | stop_path <- hierarchy(0L) 188 | path <- hierarchy(4L) 189 | 190 | local_mocked_bindings(is_fs_root = function(x) x == stop_path) 191 | 192 | expect_equal(find_root(is_rstudio_project, path = path), hierarchy(1L)) 193 | expect_equal(find_root(is_remake_project, path = path), hierarchy(2L)) 194 | expect_equal(find_root(is_projectile_project, path = path), hierarchy(3L)) 195 | }) 196 | 197 | test_that("is_svn_root", { 198 | temp_dir <- tempfile("svn") 199 | unzip("vcs/svn.zip", exdir = temp_dir) 200 | wd <- normalizePath(temp_dir, winslash = "/") 201 | 202 | hierarchy <- function(n = 0L) { 203 | do.call(file.path, list(wd, "svn", "a", "b", "c")[seq_len(n + 1L)]) 204 | } 205 | 206 | stop_path <- normalizePath(tempdir(), winslash = "/") 207 | path <- hierarchy(4L) 208 | 209 | local_mocked_bindings(is_fs_root = function(x) x == stop_path) 210 | 211 | expect_equal(find_root(is_svn_root, path = path), hierarchy(1L)) 212 | expect_equal(find_root(is_vcs_root, path = path), hierarchy(1L)) 213 | expect_error( 214 | find_root(is_svn_root, path = hierarchy(0L)), 215 | "No root directory found" 216 | ) 217 | expect_error( 218 | find_root(is_vcs_root, path = hierarchy(0L)), 219 | "No root directory found" 220 | ) 221 | }) 222 | 223 | setup_git_root <- function(separate_git_dir = FALSE) { 224 | temp_dir <- tempfile("git") 225 | unzip("vcs/git.zip", exdir = temp_dir) 226 | wd <- normalizePath(temp_dir, winslash = "/") 227 | 228 | hierarchy <- function(n = 0L) { 229 | do.call(file.path, list(wd, "git", "a", "b", "c")[seq_len(n + 1L)]) 230 | } 231 | 232 | if (separate_git_dir) { 233 | # Copy .git dir to a separate location, then make a .git file. 234 | # (other_git_folder becomes a bare git repo) 235 | old_git_location <- file.path(wd, "git", ".git") 236 | new_git_location <- file.path(wd, "other_git_folder") 237 | file.rename(old_git_location, new_git_location) 238 | writeLines(paste("gitdir:", new_git_location), old_git_location) 239 | } 240 | return(hierarchy) 241 | } 242 | 243 | test_that("is_git_root", { 244 | hierarchy <- setup_git_root(separate_git_dir = FALSE) 245 | 246 | path <- hierarchy(4L) 247 | stop_path <- normalizePath(tempdir(), winslash = "/") 248 | 249 | local_mocked_bindings(is_fs_root = function(x) x == stop_path) 250 | 251 | expect_equal(find_root(is_git_root, path = path), hierarchy(1L)) 252 | expect_equal(find_root(is_vcs_root, path = path), hierarchy(1L)) 253 | expect_error( 254 | find_root(is_git_root, path = hierarchy(0L)), 255 | "No root directory found" 256 | ) 257 | expect_error( 258 | find_root(is_vcs_root, path = hierarchy(0L)), 259 | "No root directory found" 260 | ) 261 | }) 262 | 263 | test_that("is_git_root for separated git directory", { 264 | hierarchy <- setup_git_root(separate_git_dir = TRUE) 265 | path <- hierarchy(4L) 266 | stop_path <- normalizePath(tempdir(), winslash = "/") 267 | 268 | local_mocked_bindings(is_fs_root = function(x) x == stop_path) 269 | 270 | expect_equal(find_root(is_git_root, path = path), hierarchy(1L)) 271 | expect_equal(find_root(is_vcs_root, path = path), hierarchy(1L)) 272 | expect_error( 273 | find_root(is_git_root, path = hierarchy(0L)), 274 | "No root directory found" 275 | ) 276 | expect_error( 277 | find_root(is_vcs_root, path = hierarchy(0L)), 278 | "No root directory found" 279 | ) 280 | }) 281 | 282 | test_that("finds root", { 283 | skip_on_cran() 284 | # Checks that search for root actually terminates 285 | expect_error( 286 | find_root("9259cfa7884bf51eb9dd80b52c26dcdf9cd28e82"), 287 | "No root directory found" 288 | ) 289 | }) 290 | 291 | test_that("stops if depth reached", { 292 | find_root_mocked <- find_root 293 | mock_env <- new.env() 294 | mock_env$dirname <- identity 295 | environment(find_root_mocked) <- mock_env 296 | 297 | # Checks that search for root terminates for very deep hierarchies 298 | expect_error(find_root_mocked(""), "Maximum search of [0-9]+ exceeded") 299 | }) 300 | -------------------------------------------------------------------------------- /tests/testthat/test-testthat.R: -------------------------------------------------------------------------------- 1 | test_that("is_testthat", { 2 | expect_snapshot(is_testthat) 3 | 4 | testthat_path <- normalizePath("package/tests/testthat", winslash = "/") 5 | expect_equal(is_testthat$find_file(path = "package"), testthat_path) 6 | expect_equal(is_testthat$find_file(path = "package/tests"), testthat_path) 7 | expect_equal(is_testthat$find_file(path = "package/tests/testthat"), testthat_path) 8 | }) 9 | 10 | test_that("dogfood", { 11 | expect_true(file.exists(is_testthat$find_file("hierarchy", "a", "b", "c", "d"))) 12 | }) 13 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | test_that("match_contents in non-native encoding", { 2 | desc <- c("Package: test", "Author: Kirill M\xFCller", "Encoding: latin1") 3 | Encoding(desc) <- "latin1" 4 | writeLines(desc, descfile <- tempfile(), useBytes = TRUE) 5 | expect_silent(expect_true( 6 | rprojroot:::match_contents(descfile, 7 | contents = "^Package: ", 8 | n = -1L, fixed = FALSE 9 | ) 10 | )) 11 | }) 12 | -------------------------------------------------------------------------------- /tests/testthat/vcs/git.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/vcs/git.zip -------------------------------------------------------------------------------- /tests/testthat/vcs/svn.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-lib/rprojroot/0a3b692ed80bc9929b4819b5e6bcbab22105180e/tests/testthat/vcs/svn.zip -------------------------------------------------------------------------------- /vignettes/rprojroot.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Finding files in project subdirectories" 3 | author: "Kirill Müller" 4 | date: "`r Sys.Date()`" 5 | output: rmarkdown::html_vignette 6 | vignette: > 7 | %\VignetteIndexEntry{Finding files in project subdirectories} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | %\VignetteEncoding{UTF-8} 10 | --- 11 | 12 | The *rprojroot* package solves a seemingly trivial but annoying problem 13 | that occurs sooner or later 14 | in any largish project: 15 | How to find files in subdirectories? 16 | Ideally, file paths are relative to the *project root*. 17 | 18 | Unfortunately, we cannot always be sure about the current working directory: 19 | For instance, in RStudio it's sometimes: 20 | 21 | - the project root (when running R scripts), 22 | - a subdirectory (when building vignettes), 23 | - again the project root (when executing chunks of a vignette). 24 | 25 | ```{r} 26 | basename(getwd()) 27 | ``` 28 | 29 | In some cases, it's even outside the project root. 30 | 31 | This vignette starts with a very brief summary that helps you get started, 32 | followed by a longer description of the features. 33 | 34 | ## TL;DR 35 | 36 | What is your project: An R package? 37 | 38 | ```{r} 39 | rprojroot::is_r_package 40 | ``` 41 | 42 | Or an RStudio project? 43 | 44 | ```{r} 45 | rprojroot::is_rstudio_project 46 | ``` 47 | 48 | Or something else? 49 | 50 | ```{r} 51 | rprojroot::has_file(".git/index") 52 | ``` 53 | 54 | For now, we assume it's an R package: 55 | 56 | ```{r} 57 | root <- rprojroot::is_r_package 58 | ``` 59 | 60 | The `root` object contains a function that helps locating files below the root 61 | of your package, regardless of your current working directory. 62 | If you are sure that your working directory is somewhere below your project's root, 63 | use the `root$find_file()` function. 64 | In this example here, we're starting in the `vignettes` subdirectory and find the original `DESCRIPTION` file: 65 | 66 | ```{r} 67 | basename(getwd()) 68 | readLines(root$find_file("DESCRIPTION"), 3) 69 | ``` 70 | 71 | There is one exception: if the first component passed to `find_file()` is already an absolute path. 72 | This allows safely applying this function to paths that may be absolute or relative: 73 | 74 | ```{r} 75 | path <- root$find_file() 76 | readLines(root$find_file(path, "DESCRIPTION"), 3) 77 | ``` 78 | 79 | You can also 80 | construct an accessor to your root using the `root$make_fix_file()` function: 81 | 82 | ```{r} 83 | root_file <- root$make_fix_file() 84 | ``` 85 | 86 | 87 | Note that `root_file()` is a *function* that works just like `$find_file()` but 88 | will find the files even if the current working directory is outside your project: 89 | 90 | ```{r} 91 | withr::with_dir( 92 | "../..", 93 | readLines(root_file("DESCRIPTION"), 3) 94 | ) 95 | ``` 96 | 97 | If you know the absolute path of some directory below your project, 98 | but cannot be sure of your current working directory, 99 | pass that absolute path to `root$make_fix_file()`: 100 | 101 | ```r 102 | root_file <- root$make_fix_file("C:\\Users\\User Name\\...") 103 | ``` 104 | 105 | As a last resort, you can get the path of standalone R scripts or vignettes 106 | using the `thisfile()` function: 107 | 108 | ```r 109 | root_file <- root$make_fix_file(dirname(thisfile())) 110 | ``` 111 | 112 | The remainder of this vignette describes implementation details and advanced features. 113 | 114 | 115 | ## Project root 116 | 117 | We assume a self-contained project 118 | where all files and directories are located below a common *root* directory. 119 | Also, there should be a way to unambiguously identify this root directory. 120 | (Often, the root contains a regular file whose name matches a given pattern, 121 | and/or whose contents match another pattern.) 122 | In this case, the following method reliably finds our project root: 123 | 124 | - Start the search in any subdirectory of our project 125 | - Proceed up the directory hierarchy until the root directory has been identified 126 | 127 | The Git version control system (and probably many other tools) use a similar 128 | approach: A Git command can be executed from within any subdirectory of a 129 | repository. 130 | 131 | 132 | ### A simple example 133 | 134 | The `find_root()` function implements the core functionality. 135 | It returns the path to the first directory that matches the filtering criteria, 136 | or throws an error if there is no such directory. 137 | Filtering criteria are constructed in a generic fashion using the 138 | `root_criterion()` function, 139 | the `has_file()` function constructs a criterion that checks for the presence 140 | of a file with a specific name and specific contents. 141 | 142 | ```{r} 143 | library(rprojroot) 144 | 145 | # List all files and directories below the root 146 | dir(find_root(has_file("DESCRIPTION"))) 147 | ``` 148 | 149 | #### Relative paths to a stable root 150 | 151 | Here we illustrate the power of *rprojroot* by demonstrating how to access the same file from two different working directories. Let your project be a package called `pkgname` and consider the desired file `rrmake.R` at `pkgname/R/rrmake.R`. First, we show how to access from the `vignettes` directory, and then from the `tests/testthat` directory. 152 | 153 | 154 | ##### Example A: From `vignettes` 155 | 156 | When your working directory is `pkgname/vignettes`, you can access the `rrmake.R` file by: 157 | 158 | 1. Supplying a pathname relative to your working directory. Here's two ways to do that: 159 | 160 | ```{r, eval = FALSE} 161 | rel_path_from_vignettes <- "../R/rrmake.R" 162 | rel_path_from_vignettes <- file.path("..", "R", "rrmake.R") ## identical 163 | ``` 164 | 165 | 2. Supplying a pathname to the file relative from the root of the package, e.g., 166 | 167 | ```{r, eval = FALSE} 168 | rel_path_from_root <- "R/rrmake.R" 169 | rel_path_from_root <- file.path("R", "rrmake.R") ## identical 170 | ``` 171 | 172 | This second method requires finding the root of the package, which can be done with the `has_file()` function: 173 | 174 | ```{r} 175 | has_file("DESCRIPTION") 176 | ``` 177 | 178 | So, using *rprojroot* you can specify the path relative from root in the following manner: 179 | 180 | ```{r} 181 | # Specify a path/to/file relative to the root 182 | rel_path_from_root <- find_root_file("R", "rrmake.R", criterion = has_file("DESCRIPTION")) 183 | ``` 184 | 185 | 186 | ##### Example B: From `tests/testthat` 187 | 188 | When your working directory is `pkgname/tests/testthat`, you can access the `rrmake.R` file by: 189 | 190 | 1. Supplying a pathname relative to your working directory. 191 | 192 | ```{r, eval = FALSE} 193 | rel_path_from_testthat <- "../../R/rrmake.R" 194 | ``` 195 | 196 | Note that this is different than in the previous example! However, the second method is the same... 197 | 198 | 2. Supplying a pathname to the file relative from the root of the package. With *rprojroot*, this is the exact same as in the previous example. 199 | 200 | 201 | ```{r} 202 | # Specify a path/to/file relative to the root 203 | rel_path_from_root <- find_root_file("R", "rrmake.R", criterion = has_file("DESCRIPTION")) 204 | ``` 205 | 206 | ##### Summary of Examples A and B 207 | 208 | Since Examples A and B used different working directories, `rel_path_from_vignettes` and `rel_path_from_testthat` were different. This is an issue when trying to re-use the same code. This issue is solved by using *rprojroot*: the function `find_root_file()` finds a file relative from the root, where the root is determined from checking the criterion with `has_file()`. 209 | 210 | 211 | Note that the follow code produces identical results when building the vignette *and* when sourcing the chunk in RStudio, provided that the current working directory is the project root or anywhere below. So, we can check to make sure that *rprojroot* has successfully determined the correct path: 212 | 213 | ```{r} 214 | # Specify a path/to/file relative to the root 215 | rel_path_from_root <- find_root_file("R", "rrmake.R", criterion = has_file("DESCRIPTION")) 216 | 217 | # Find a file relative to the root 218 | file.exists(rel_path_from_root) 219 | ``` 220 | 221 | 222 | ### Criteria 223 | 224 | The `has_file()` function (and the more general `root_criterion()`) 225 | both return an S3 object of class `root_criterion`: 226 | 227 | ```{r} 228 | has_file("DESCRIPTION") 229 | ``` 230 | 231 | In addition, character values are coerced to `has_file` criteria by default, this coercion is applied automatically by `find_root()`. 232 | (This feature is used by the introductory example.) 233 | 234 | ```{r} 235 | as_root_criterion("DESCRIPTION") 236 | ``` 237 | 238 | The return value of these functions can be stored and reused; 239 | in fact, the package provides `r length(criteria)` such criteria: 240 | 241 | ```{r} 242 | criteria 243 | ``` 244 | 245 | Defining new criteria is easy: 246 | 247 | ```{r} 248 | has_license <- has_file("LICENSE") 249 | has_license 250 | 251 | is_projecttemplate_project <- has_file("config/global.dcf", "^version: ") 252 | is_projecttemplate_project 253 | ``` 254 | 255 | You can also combine criteria via the `|` operator: 256 | 257 | ```{r} 258 | is_r_package | is_rstudio_project 259 | ``` 260 | 261 | 262 | 263 | ### Shortcuts 264 | 265 | To avoid specifying the search criteria for the project root every time, 266 | shortcut functions can be created. 267 | The `find_package_root_file()` is a shortcut for 268 | `find_root_file(..., criterion = is_r_package)`: 269 | 270 | ```{r} 271 | # Print first lines of the source for this document 272 | head(readLines(find_package_root_file("vignettes", "rprojroot.Rmd"))) 273 | ``` 274 | 275 | To save typing effort, define a shorter alias: 276 | 277 | ```{r} 278 | P <- find_package_root_file 279 | 280 | # Use a shorter alias 281 | file.exists(P("vignettes", "rprojroot.Rmd")) 282 | ``` 283 | 284 | Each criterion actually contains a function that allows finding a file below the root specified by this criterion. 285 | As our project does not have a file named `LICENSE`, querying the root results in an error: 286 | 287 | ```{r error = TRUE} 288 | # Use the has_license criterion to find the root 289 | R <- has_license$find_file 290 | R 291 | 292 | # Our package does not have a LICENSE file, trying to find the root results in an error 293 | R() 294 | ``` 295 | 296 | 297 | ### Fixed root 298 | 299 | We can also create a function 300 | that computes a path relative to the root *at creation time*. 301 | 302 | ```{r eval = (Sys.getenv("IN_PKGDOWN") != "")} 303 | # Define a function that computes file paths below the current root 304 | F <- is_r_package$make_fix_file() 305 | F 306 | 307 | # Show contents of the NAMESPACE file in our project 308 | readLines(F("NAMESPACE")) 309 | ``` 310 | 311 | This is a more robust alternative to `$find_file()`, because it *fixes* the project 312 | directory when `$make_fix_file()` is called, instead of searching for it every 313 | time. (For that reason it is also slightly faster, but I doubt this matters 314 | in practice.) 315 | 316 | This function can be used even if we later change the working directory to somewhere outside the project: 317 | 318 | ```{r eval = (Sys.getenv("IN_PKGDOWN") != "")} 319 | # Print the size of the namespace file, working directory outside the project 320 | withr::with_dir( 321 | "../..", 322 | file.size(F("NAMESPACE")) 323 | ) 324 | ``` 325 | 326 | The `make_fix_file()` member function also accepts an optional `path` argument, 327 | in case you know your project's root but the current working directory is somewhere outside. 328 | The path to the current script or `knitr` document can be obtained using the `thisfile()` function, but it's much easier and much more robust to just run your scripts with the working directory somewhere below your project root. 329 | 330 | 331 | ## `testthat` files 332 | 333 | Tests run with [`testthat`](https://cran.r-project.org/package=testthat) 334 | commonly use files that live below the `tests/testthat` directory. 335 | Ideally, this should work in the following situation: 336 | 337 | - During package development (working directory: package root) 338 | - When testing with `devtools::test()` (working directory: `tests/testthat`) 339 | - When running `R CMD check` (working directory: a renamed recursive copy of `tests`) 340 | 341 | The `is_testthat` criterion allows robust lookup of test files. 342 | 343 | ```{r} 344 | is_testthat 345 | ``` 346 | 347 | The example code below lists all files in the 348 | [hierarchy](https://github.com/r-lib/rprojroot/tree/main/tests/testthat/hierarchy) 349 | test directory. 350 | It uses two project root lookups in total, 351 | so that it also works when rendering the vignette (*sigh*): 352 | 353 | ```{r} 354 | dir(is_testthat$find_file("hierarchy", path = is_r_package$find_file())) 355 | ``` 356 | 357 | ### Another example: custom testing utilities 358 | 359 | The hassle of using saved data files for testing is made even easier by using *rprojroot* in a utility function. For example, suppose you have a testing file at `tests/testthat/test_my_fun.R` which tests the `my_fun()` function: 360 | 361 | ```{r, eval = FALSE} 362 | my_fun_run <- do.call(my_fun, my_args) 363 | 364 | testthat::test_that( 365 | "my_fun() returns expected output", 366 | testthat::expect_equal( 367 | my_fun_run, 368 | expected_output 369 | ) 370 | ) 371 | ``` 372 | 373 | There are two pieces of information that you'll need every time `test_my_fun.R` is run: `my_args` and `expected_output`. Typically, these objects are saved to `.Rdata` files and saved to the same subdirectory. For example, you could save them to `my_args.Rdata` and `expected_output.Rdata` under the `tests/testthat/testing_data` subdirectory. And, to find them easily in any contexts, you can use *rprojroot*! 374 | 375 | Since all of the data files live in the same subdirectory, you can create a utility function `get_my_path()` that will always look in that directory for these types of files. And, since the *testthat* package will look for and source the `tests/testthat/helper.R` file before running any tests, you can place a `get_my_path()` in this file and use it throughout your tests: 376 | 377 | ```{r, eval = FALSE} 378 | ## saved to tests/testthat/helper.R 379 | get_my_path <- function(file_name) { 380 | rprojroot::find_testthat_root_file( 381 | "testing_data", filename 382 | ) 383 | } 384 | ``` 385 | 386 | Now you can ask `get_my_path()` to find your important data files by using the function within your test scripts! 387 | 388 | ```{r, eval = FALSE} 389 | ## Find the correct path with your custom rprojroot helper function 390 | path_to_my_args_file <- get_my_path("my_args.Rdata") 391 | 392 | ## Load the input arguments 393 | load(file = path_to_my_args_file) 394 | 395 | ## Run the function with those arguments 396 | my_fun_run <- do.call(my_fun, my_args) 397 | 398 | ## Load the historical expectation with the helper 399 | load(file = get_my_path("expected_output.Rdata")) 400 | 401 | ## Pass all tests and achieve nirvana 402 | testthat::test_that( 403 | "my_fun() returns expected output", 404 | testthat::expect_equal( 405 | my_fun_run, 406 | expected_output 407 | ) 408 | ) 409 | ``` 410 | 411 | For an example in the wild, see the [`test_sheet()` function](https://github.com/tidyverse/readxl/blob/0d9ad4f570f6580ff716e0e9ba5048447048e9f0/tests/testthat/helper.R#L1-L3) in the *readxl* package. 412 | 413 | ## Summary 414 | 415 | The *rprojroot* package allows easy access to files below a project root 416 | if the project root can be identified easily, e.g. if it is the only directory 417 | in the whole hierarchy that contains a specific file. 418 | This is a robust solution for finding files in largish projects 419 | with a subdirectory hierarchy if the current working directory cannot be assumed 420 | fixed. 421 | (However, at least initially, the current working directory must be 422 | somewhere below the project root.) 423 | 424 | 425 | ## Acknowledgement 426 | 427 | This package was inspired by the gist 428 | ["Stop the working directory insanity"](https://gist.github.com/jennybc/362f52446fe1ebc4c49f) 429 | by Jennifer Bryan, and by the way Git knows where its files are. 430 | --------------------------------------------------------------------------------