├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ ├── extendr_update.yaml │ ├── lint.yaml │ ├── pkgdown.yaml │ ├── pr-commands.yaml │ ├── test-coverage.yaml │ └── test_pkg_gen.yaml ├── .gitignore ├── .lintr ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── clean.R ├── cran-compliance.R ├── create_extendr_package.R ├── eval.R ├── features.R ├── find_exports.R ├── find_extendr.R ├── function_options.R ├── generate_toml.R ├── helpers.r ├── import-standalone-obj-type.R ├── import-standalone-types-check.R ├── knitr_engine.R ├── license_note.R ├── make_module_macro.R ├── read_cargo_metadata.R ├── register_extendr.R ├── rextendr.R ├── rextendr_document.R ├── run_cargo.R ├── rust_sitrep.R ├── sanitize_code.R ├── setup.R ├── source.R ├── standalone-purrr.R ├── toml_serialization.R ├── track_rust_source.R ├── use_crate.R ├── use_extendr.R ├── use_msrv.R ├── use_vscode.R ├── utils.R ├── write_file.R └── zzz.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── codecov.yml ├── cran-comments.md ├── inst ├── rstudio │ └── templates │ │ └── project │ │ └── extendr.dcf └── templates │ ├── Cargo.toml │ ├── Makevars.in │ ├── Makevars.win.in │ ├── _gitignore │ ├── config.R │ ├── configure │ ├── configure.win │ ├── entrypoint.c │ ├── extendr-wrappers.R │ ├── lib.rs │ ├── msrv.R │ ├── settings.json │ └── win.def ├── man ├── clean.Rd ├── cran.Rd ├── document.Rd ├── eng_extendr.Rd ├── figures │ └── rextendr-logo.png ├── inf_dev_extendr_used.Rd ├── local_quiet_cli.Rd ├── make_module_macro.Rd ├── read_cargo_metadata.Rd ├── register_extendr.Rd ├── rextendr.Rd ├── rust_eval.Rd ├── rust_sitrep.Rd ├── rust_source.Rd ├── to_toml.Rd ├── use_crate.Rd ├── use_extendr.Rd ├── use_msrv.Rd ├── use_vscode.Rd ├── vendor_pkgs.Rd └── write_license_note.Rd ├── principles.md ├── rextendr.Rproj ├── tests ├── data │ ├── inner_1 │ │ └── rust_source.rs │ ├── inner_2 │ │ └── rust_source.rs │ ├── ndarray_example.rs │ ├── rust_source.rs │ └── test-knitr-engine-source-01.Rmd ├── testthat.R └── testthat │ ├── _snaps │ ├── cran-compliance.md │ ├── knitr-engine.md │ ├── license_note.md │ ├── rust-sitrep.md │ └── use_extendr.md │ ├── helper-toolchain.R │ ├── helper.R │ ├── setup.R │ ├── test-clean.R │ ├── test-cran-compliance.R │ ├── test-document.R │ ├── test-eval.R │ ├── test-extendr_function_options.R │ ├── test-find_extendr.R │ ├── test-knitr-engine.R │ ├── test-license_note.R │ ├── test-make-module-macro.R │ ├── test-name-override.R │ ├── test-optional-features.R │ ├── test-pretty_rel_path.R │ ├── test-read_cargo_metadata.R │ ├── test-rstudio-template.R │ ├── test-rust-sitrep.R │ ├── test-source.R │ ├── test-toml.R │ ├── test-use_crate.R │ ├── test-use_dev_extendr.R │ ├── test-use_extendr.R │ ├── test-use_msrv.R │ ├── test-use_vscode.R │ └── test-utils.R └── vignettes ├── .gitignore ├── package.Rmd ├── rmarkdown.Rmd └── setting_up_rust.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^rextendr\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^vignettes/rmarkdown.Rmd$ 6 | ^\.github$ 7 | ^docs$ 8 | ^_pkgdown\.yml$ 9 | ^principles\.md$ 10 | ^CODE-OF-CONDUCT\.md$ 11 | ^CONTRIBUTING\.md$ 12 | ^cran-comments\.md$ 13 | ^CRAN-RELEASE$ 14 | ^\.lintr$ 15 | ^codecov\.yml$ 16 | ^\.vscode$ 17 | ^\.idea$ 18 | ^inst/libgcc_mock/libgcc_eh.a$ 19 | ^CRAN-SUBMISSION$ 20 | ^vignettes/articles$ 21 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main, master] 4 | pull_request: 5 | branches: [main, master] 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | - ready_for_review 11 | 12 | name: R-CMD-check 13 | 14 | jobs: 15 | R-CMD-check: 16 | runs-on: ${{ matrix.config.os }} 17 | 18 | name: R-CMD-Check ${{ matrix.config.os }} (${{ matrix.config.r }} - ${{ matrix.config.rust-version }}) 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | config: 24 | - {os: windows-latest, r: 'release', rust-version: 'stable-msvc', rust-target: 'x86_64-pc-windows-gnu', rtools-version: '45' } 25 | - {os: windows-latest, r: 'devel', rust-version: 'stable-msvc', rust-target: 'x86_64-pc-windows-gnu', rtools-version: '45' } 26 | - {os: windows-latest, r: 'oldrel', rust-version: 'stable-msvc', rust-target: 'x86_64-pc-windows-gnu', rtools-version: '44' } 27 | 28 | - {os: macOS-latest, r: 'release', rust-version: 'stable' } 29 | 30 | - {os: ubuntu-latest, r: 'release', rust-version: 'stable' } 31 | - {os: ubuntu-latest, r: 'devel', rust-version: 'stable' } 32 | - {os: ubuntu-latest, r: 'oldrel', rust-version: 'stable' } 33 | 34 | env: 35 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 36 | R_KEEP_PKG_SOURCE: yes 37 | REXTENDR_SKIP_DEV_TESTS: TRUE # TODO: Remove this when extendr/libR-sys issue is resolved 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - uses: dtolnay/rust-toolchain@master 43 | with: 44 | toolchain: ${{ matrix.config.rust-version }} 45 | targets: ${{ matrix.config.rust-target }} 46 | 47 | - uses: baptiste0928/cargo-install@v3 48 | if: matrix.config.r == 'release' 49 | with: 50 | crate: cargo-license 51 | 52 | - uses: r-lib/actions/setup-r@v2 53 | with: 54 | r-version: ${{ matrix.config.r }} 55 | rtools-version: ${{ matrix.config.rtools-version }} 56 | use-public-rspm: true 57 | 58 | - uses: r-lib/actions/setup-r-dependencies@v2 59 | with: 60 | cache-version: 2 61 | extra-packages: rcmdcheck 62 | 63 | - uses: r-lib/actions/check-r-package@v2 64 | with: 65 | error-on: '"note"' 66 | -------------------------------------------------------------------------------- /.github/workflows/extendr_update.yaml: -------------------------------------------------------------------------------- 1 | name: Run Tests on extendr Update 2 | 3 | on: 4 | repository_dispatch: 5 | types: [extendr-pr-merged] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | run_rextendr_tests: 10 | uses: ./.github/workflows/R-CMD-check.yaml 11 | -------------------------------------------------------------------------------- /.github/workflows/lint.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 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | types: 9 | - opened 10 | - reopened 11 | - synchronize 12 | - ready_for_review 13 | 14 | name: lint 15 | 16 | jobs: 17 | lint: 18 | runs-on: ubuntu-latest 19 | env: 20 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: r-lib/actions/setup-r@v2 25 | with: 26 | use-public-rspm: true 27 | 28 | - uses: r-lib/actions/setup-r-dependencies@v2 29 | with: 30 | install-pandoc: false 31 | install-quarto: false 32 | extra-packages: any::lintr, local::. 33 | needs: lint 34 | 35 | - name: Lint 36 | run: lintr::lint_package() 37 | shell: Rscript {0} 38 | env: 39 | LINTR_ERROR_ON_LINT: true 40 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.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 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | types: 9 | - opened 10 | - reopened 11 | - synchronize 12 | - ready_for_review 13 | release: 14 | types: [published] 15 | workflow_dispatch: 16 | 17 | name: pkgdown 18 | 19 | jobs: 20 | pkgdown: 21 | runs-on: ubuntu-latest 22 | env: 23 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Set up Rust 28 | uses: dtolnay/rust-toolchain@stable 29 | 30 | - uses: r-lib/actions/setup-r@v2 31 | with: 32 | use-public-rspm: true 33 | 34 | - uses: r-lib/actions/setup-r-dependencies@v2 35 | with: 36 | install-pandoc: false 37 | install-quarto: false 38 | extra-packages: any::pkgdown, local::. 39 | needs: website 40 | 41 | - name: Build site 42 | run: Rscript -e 'pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)' 43 | 44 | - name: Deploy to GitHub pages 🚀 45 | if: github.event_name != 'pull_request' 46 | uses: JamesIves/github-pages-deploy-action@4.1.4 47 | with: 48 | clean: false 49 | branch: gh-pages 50 | folder: docs 51 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.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 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | name: Commands 8 | 9 | jobs: 10 | document: 11 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }} 12 | name: document 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: r-lib/actions/pr-fetch@v2 20 | with: 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - uses: r-lib/actions/setup-r@v2 24 | with: 25 | use-public-rspm: true 26 | 27 | - uses: r-lib/actions/setup-r-dependencies@v2 28 | with: 29 | install-pandoc: false 30 | install-quarto: false 31 | extra-packages: any::roxygen2 32 | needs: pr-document 33 | 34 | - name: Document 35 | run: Rscript -e 'roxygen2::roxygenise()' 36 | 37 | - name: commit 38 | run: | 39 | git config --local user.name "$GITHUB_ACTOR" 40 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 41 | git add man/\* NAMESPACE 42 | git commit -m 'Document' 43 | 44 | - uses: r-lib/actions/pr-push@v2 45 | with: 46 | repo-token: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | style: 49 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }} 50 | name: style 51 | runs-on: ubuntu-latest 52 | env: 53 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 54 | steps: 55 | - uses: actions/checkout@v3 56 | 57 | - uses: r-lib/actions/pr-fetch@v2 58 | with: 59 | repo-token: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | - uses: r-lib/actions/setup-r@v2 62 | 63 | - name: Install dependencies 64 | run: Rscript -e 'install.packages("styler")' 65 | 66 | - name: Style 67 | run: Rscript -e 'styler::style_pkg()' 68 | 69 | - name: commit 70 | run: | 71 | git config --local user.name "$GITHUB_ACTOR" 72 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 73 | git add \*.R 74 | git commit -m 'Style' 75 | 76 | - uses: r-lib/actions/pr-push@v2 77 | with: 78 | repo-token: ${{ secrets.GITHUB_TOKEN }} 79 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.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 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | 8 | name: test-coverage.yaml 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | test-coverage: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 17 | REXTENDR_SKIP_DEV_TESTS: TRUE # TODO: Remove this when extendr/libR-sys issue is resolved 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: r-lib/actions/setup-r@v2 23 | with: 24 | use-public-rspm: true 25 | 26 | - uses: r-lib/actions/setup-r-dependencies@v2 27 | with: 28 | extra-packages: any::covr, any::xml2 29 | needs: coverage 30 | 31 | - name: Test coverage 32 | run: | 33 | cov <- covr::package_coverage( 34 | quiet = FALSE, 35 | clean = FALSE, 36 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 37 | ) 38 | covr::to_cobertura(cov) 39 | shell: Rscript {0} 40 | 41 | - uses: codecov/codecov-action@v4 42 | with: 43 | # Fail if error if not on PR, or if on PR and token is given 44 | fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} 45 | file: ./cobertura.xml 46 | plugin: noop 47 | disable_search: true 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | 50 | - name: Show testthat output 51 | if: always() 52 | run: | 53 | ## -------------------------------------------------------------------- 54 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 55 | shell: bash 56 | 57 | - name: Upload test results 58 | if: failure() 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: coverage-test-failures 62 | path: ${{ runner.temp }}/package 63 | -------------------------------------------------------------------------------- /.github/workflows/test_pkg_gen.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main, master] 4 | pull_request: 5 | branches: [main, master] 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | - ready_for_review 11 | 12 | name: Test package generation 13 | 14 | jobs: 15 | R-CMD-check: 16 | runs-on: ${{ matrix.config.os }} 17 | 18 | name: PkgGen ${{ matrix.config.os }} (${{ matrix.config.r }} - ${{ matrix.config.rust-version }}) 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | config: 24 | - {os: windows-latest, r: 'release', rust-version: 'stable-msvc', rust-target: 'x86_64-pc-windows-gnu'} 25 | - {os: windows-latest, r: 'devel', rust-version: 'stable-msvc', rust-target: 'x86_64-pc-windows-gnu'} 26 | - {os: windows-latest, r: 'oldrel', rust-version: 'stable-msvc', rust-target: 'x86_64-pc-windows-gnu', rtools-version: '43'} 27 | 28 | - {os: macOS-latest, r: 'release', rust-version: 'stable'} 29 | 30 | - {os: ubuntu-latest, r: 'release', rust-version: 'stable'} 31 | - {os: ubuntu-latest, r: 'devel', rust-version: 'stable'} 32 | - {os: ubuntu-latest, r: 'oldrel', rust-version: 'stable'} 33 | 34 | env: 35 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 36 | RSPM: ${{ matrix.config.rspm }} 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - uses: dtolnay/rust-toolchain@master 42 | with: 43 | toolchain: ${{ matrix.config.rust-version }} 44 | targets: ${{ matrix.config.rust-target }} 45 | 46 | - name: Set up R 47 | uses: r-lib/actions/setup-r@v2 48 | with: 49 | r-version: ${{ matrix.config.r }} 50 | rtools-version: ${{ matrix.config.rtools-version }} 51 | use-public-rspm: true 52 | 53 | - uses: r-lib/actions/setup-r-dependencies@v2 54 | with: 55 | install-pandoc: false 56 | install-quarto: false 57 | # increment this version number when we need to clear the cache 58 | cache-version: 3 59 | extra-packages: rcmdcheck, devtools, usethis 60 | 61 | - name: Test package generation 62 | env: 63 | _R_CHECK_CRAN_INCOMING_REMOTE_: false 64 | run: | 65 | # preperation 66 | remotes::install_local(force = TRUE) 67 | temp_dir <- tempdir() 68 | pkg_dir <- file.path(temp_dir, "testpkg") 69 | dir.create(pkg_dir, recursive = TRUE) 70 | setwd(pkg_dir) 71 | Sys.setenv(REXTENDR_TEST_PKG_ROOT = getwd()) 72 | devtools::create(".") 73 | usethis::proj_activate(".") 74 | rextendr::use_extendr() 75 | usethis::use_mit_license() 76 | usethis::use_testthat() 77 | brio::write_lines( 78 | c( 79 | "test_that(\"`hello_world()` works\", {", 80 | " expect_equal(hello_world(), \"Hello world!\")", 81 | "})", 82 | "test_that(\"`rextendr::use_extendr()` works\", {", 83 | " wrap_path <- file.path(Sys.getenv(\"REXTENDR_TEST_PKG_ROOT\"), \"R\", \"extendr-wrappers.R\")", 84 | " expect_equal(readLines(wrap_path, 1), \"# Generated by extendr: Do not edit by hand\")", 85 | "})" 86 | ), 87 | file.path("tests", "testthat", "test-hello.R") 88 | ) 89 | 90 | # TODO: allow warnings on oldrel (cf., https://stat.ethz.ch/pipermail/r-package-devel/2023q2/009229.html) 91 | if (.Platform$OS.type == "windows" && getRversion() < "4.3.0") { 92 | error_on <- "error" 93 | } else { 94 | error_on <- "warning" 95 | } 96 | 97 | # check if rextendr::document() compiles and generates wrappers properly 98 | rextendr::document() 99 | rcmdcheck::rcmdcheck( 100 | path = ".", 101 | args = c("--no-manual", "--as-cran"), 102 | error_on = error_on, 103 | check_dir = "check_use_extendr" 104 | ) 105 | 106 | library_path <- rextendr:::get_library_path() 107 | library_mtime_before <- file.info(library_path)$mtime 108 | wrappers_path <- file.path("R", "extendr-wrappers.R") 109 | wrappers_mtime_before <- file.info(wrappers_path)$mtime 110 | 111 | # check if rextendr::document() don't regenerate wrappers when unnecessary 112 | rextendr::document() 113 | stopifnot(library_mtime_before == file.info(library_path)$mtime) 114 | stopifnot(wrappers_mtime_before == file.info(wrappers_path)$mtime) 115 | 116 | # check if force = TRUE forces regenerating wrappers, but not compile 117 | rextendr::register_extendr(force = TRUE) 118 | stopifnot(library_mtime_before == file.info(library_path)$mtime) 119 | stopifnot(wrappers_mtime_before < file.info(wrappers_path)$mtime) 120 | 121 | wrappers_mtime_before <- file.info(wrappers_path)$mtime 122 | 123 | # check if compile = TRUE forces compile, and accordingly the wrapper generation 124 | rextendr::register_extendr(compile = TRUE) 125 | 126 | stopifnot(library_mtime_before < file.info(library_path)$mtime) 127 | stopifnot(wrappers_mtime_before < file.info(wrappers_path)$mtime) 128 | shell: Rscript {0} 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # History files 2 | .Rhistory 3 | .Rapp.history 4 | 5 | # Session Data files 6 | .RData 7 | 8 | # User-specific files 9 | .Ruserdata 10 | 11 | # Example code in package build process 12 | *-Ex.R 13 | 14 | # Output files from R CMD build 15 | /*.tar.gz 16 | 17 | # Output files from R CMD check 18 | /*.Rcheck/ 19 | 20 | # RStudio files 21 | .Rproj.user/ 22 | 23 | # produced vignettes 24 | vignettes/*.html 25 | vignettes/*.pdf 26 | 27 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 28 | .httr-oauth 29 | 30 | # knitr and R markdown default cache directories 31 | *_cache/ 32 | /cache/ 33 | 34 | # Temporary files created by R markdown 35 | *.utf8.md 36 | *.knit.md 37 | 38 | # R Environment Variables 39 | .Renviron 40 | .Rproj.user 41 | 42 | **/.vscode/* 43 | 44 | /inst/libgcc_mock 45 | 46 | **/.idea/* 47 | 48 | CRAN-SUBMISSION -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | linters: linters_with_defaults( 2 | line_length_linter(120), 3 | object_name_linter = NULL, 4 | commented_code_linter = NULL 5 | ) 6 | encoding: "UTF-8" 7 | exclusions: list( 8 | "R/import-standalone-obj-type.R" 9 | ) 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions to the rextendr project. Contributions come in many forms. Please carefully read and follow these guidelines. This will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing this project. 4 | 5 | ## Quicklinks 6 | 7 | * [Code of Conduct](#code-of-conduct) 8 | * [Getting Started](#getting-started) 9 | * [Issues](#issues) 10 | * [Pull Requests](#pull-requests) 11 | * [Code Style](#code-style) 12 | * [Getting Help](#getting-help) 13 | * [Authorship](#authorship) 14 | * [Attribution](#attribution) 15 | 16 | ## Code of Conduct 17 | 18 | We take our open source community seriously and hold ourselves and other contributors to high standards of communication. By participating and contributing to this project, you agree to uphold our [Code of Conduct.](https://github.com/extendr/extendr/blob/master/CODE-OF-CONDUCT.md) 19 | 20 | ## Getting Started 21 | 22 | Contributions can be made via Issues and Pull Requests (PRs). A few general guidelines cover both: 23 | 24 | - Please search for existing Issues and PRs before creating your own. 25 | - We work hard to makes sure issues are handled in a timely manner but, depending on the problem and maintainer availability, it could take a while to investigate the problem. A friendly ping in the comment thread can help draw attention if an issue has not received any attention for a while. Please keep in mind that all contributors to this project are volunteers and may have other commitments they need to attend to. 26 | 27 | ### Issues 28 | 29 | Issues should be used to report problems with the library, request a new feature, or to discuss potential changes before a PR is created. Please **do not** use Issues to request user support. 30 | 31 | Whenever possible, please provide a minimal reproducible example (reprex) to any bug report that you are filing. The more minimal your example, the more likely that somebody else can figure out what the problem is, so please remove any code that isn't relevant to the problem you are reporting. 32 | 33 | Please keep issues focused on one particular problem. Don't feel shy about opening multiple issues if you're encountering more than one problem. 34 | 35 | If you find an Issue that addresses the problem you're having, please add your own reproduction information to the existing issue rather than creating a new one. Adding a [reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) can also help be indicating to our maintainers that a particular problem is affecting more than just the reporter. 36 | 37 | ### Pull Requests 38 | 39 | PRs are always welcome and can be a quick way to get your fix or improvement slated for the next release. However, please always open an Issue before submitting a PR. 40 | 41 | In general, PRs should: 42 | 43 | - Address a single concern in the least number of changed lines as possible. 44 | - Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both. 45 | - Add unit or integration tests for fixed or changed functionality. 46 | - Include documentation. 47 | - Indicate which Issue they address by using the words `Closes #` or `Fixes #` in the body of the PR and/or the git commit message. (See the [GitHub Documentation](https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for details about linking PRs to Issues and automatically closing Issues when merging PRs.) 48 | 49 | 50 | In general, we follow the [GitHub flow](https://guides.github.com/introduction/flow/index.html) development model: 51 | 52 | 1. Fork the repository to your own Github account 53 | 2. Clone the project to your machine 54 | 3. Create a branch locally with a succinct but descriptive name 55 | 4. Commit changes to the branch 56 | 5. Push changes to your fork 57 | 6. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. 58 | 59 | ### Code style 60 | * New code should follow the tidyverse [style guide](https://style.tidyverse.org). 61 | You can use the [styler](https://CRAN.R-project.org/package=styler) package to apply these styles, but please don't restyle code that has nothing to do with your PR. 62 | 63 | * We use [roxygen2](https://cran.r-project.org/package=roxygen2), with [Markdown syntax](https://cran.r-project.org/web/packages/roxygen2/vignettes/rd-formatting.html), for documentation. 64 | 65 | * We use [testthat](https://cran.r-project.org/package=testthat) for unit tests. 66 | Contributions with test cases included are easier to accept. 67 | 68 | ## Getting Help 69 | 70 | Please join us on our [Discord server](https://discord.gg/7hmApuc) for general conversations and questions that don't belong into a GitHub issue. 71 | 72 | ## Authorship 73 | 74 | Contributors who have made multiple, sustained, and/or non-trivial contributions to the project may be added to the author list. New author names will always be added at the end of the list, so that author order reflects chronological order of joining the project. All authorship decisions are at the discretion of the current maintainers of the project. 75 | 76 | ## Attribution 77 | 78 | This document was adapted from the [General Contributing Guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) of the auth0 project. 79 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: rextendr 2 | Title: Call Rust Code from R using the 'extendr' Crate 3 | Version: 0.4.0.9000 4 | Authors@R: 5 | c(person(given = "Claus O.", 6 | family = "Wilke", 7 | role = "aut", 8 | email = "wilke@austin.utexas.edu", 9 | comment = c(ORCID = "0000-0002-7470-9261")), 10 | person(given = "Andy", 11 | family = "Thomason", 12 | role = "aut", 13 | email = "andy@andythomason.com"), 14 | person(given = "Mossa M.", 15 | family = "Reimert", 16 | role = "aut", 17 | email = "mossa@sund.ku.dk"), 18 | person(given = "Ilia", 19 | family = "Kosenkov", 20 | role = c("aut", "cre"), 21 | email = "ilia.kosenkov@outlook.com", 22 | comment = c(ORCID = "0000-0001-5563-7840")), 23 | person(given = "Malcolm", 24 | family = "Barrett", 25 | role = "aut", 26 | email = "malcolmbarrett@gmail.com", 27 | comment = c(ORCID = "0000-0003-0299-5825")), 28 | person(given = "Josiah", 29 | family = "Parry", 30 | role = "ctb", 31 | email = "josiah.parry@gmail.con", 32 | comment = c(ORCID = "0000-0001-9910-865X")), 33 | person(given = "Kenneth", 34 | family = "Vernon", 35 | role = "ctb", 36 | email = "kenneth.b.vernon@gmail.com", 37 | comment = c(ORCID = "0000-0003-0098-5092")), 38 | person(given = "Alberson", 39 | family = "Miranda", 40 | role = "ctb", 41 | email = "albersonmiranda@hotmail.com", 42 | comment = c(ORCID = "0000-0001-9252-4175"))) 43 | Description: Provides functions to compile and load Rust code from R, similar 44 | to how 'Rcpp' or 'cpp11' allow easy interfacing with C++ code. Also provides 45 | helper functions to create R packages that use Rust code. Under the hood, 46 | the Rust crate 'extendr' is used to do all the heavy lifting. 47 | License: MIT + file LICENSE 48 | URL: https://extendr.github.io/rextendr/, https://github.com/extendr/rextendr 49 | BugReports: https://github.com/extendr/rextendr/issues 50 | Depends: 51 | R (>= 4.1) 52 | Imports: 53 | brio, 54 | callr, 55 | cli, 56 | desc, 57 | dplyr, 58 | glue (>= 1.7.0), 59 | jsonlite, 60 | pkgbuild (>= 1.4.0), 61 | processx, 62 | rlang (>= 1.0.5), 63 | rprojroot, 64 | stringi, 65 | vctrs, 66 | withr 67 | Suggests: 68 | devtools, 69 | rcmdcheck, 70 | knitr, 71 | lintr, 72 | rmarkdown, 73 | testthat (>= 3.1.7), 74 | usethis 75 | VignetteBuilder: 76 | knitr 77 | Config/testthat/edition: 3 78 | Config/testthat/parallel: true 79 | Encoding: UTF-8 80 | Roxygen: list(markdown = TRUE) 81 | RoxygenNote: 7.3.2 82 | SystemRequirements: Rust 'cargo'; the crate 'libR-sys' must compile 83 | without error 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2020 2 | COPYRIGHT HOLDER: rextendr authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 rextendr 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(format_toml,"NULL") 4 | S3method(format_toml,character) 5 | S3method(format_toml,data.frame) 6 | S3method(format_toml,default) 7 | S3method(format_toml,double) 8 | S3method(format_toml,integer) 9 | S3method(format_toml,list) 10 | S3method(format_toml,logical) 11 | S3method(format_toml,name) 12 | export(clean) 13 | export(document) 14 | export(eng_extendr) 15 | export(eng_extendrsrc) 16 | export(make_module_macro) 17 | export(read_cargo_metadata) 18 | export(register_extendr) 19 | export(rust_eval) 20 | export(rust_function) 21 | export(rust_sitrep) 22 | export(rust_source) 23 | export(to_toml) 24 | export(use_crate) 25 | export(use_extendr) 26 | export(use_msrv) 27 | export(use_positron) 28 | export(use_vscode) 29 | export(vendor_pkgs) 30 | export(write_license_note) 31 | importFrom(dplyr,mutate) 32 | importFrom(glue,glue) 33 | importFrom(glue,glue_collapse) 34 | importFrom(rlang,"%||%") 35 | importFrom(rlang,.data) 36 | importFrom(rlang,.env) 37 | importFrom(rlang,as_function) 38 | importFrom(rlang,as_label) 39 | importFrom(rlang,as_name) 40 | importFrom(rlang,caller_env) 41 | importFrom(rlang,dots_list) 42 | importFrom(rlang,enquo) 43 | importFrom(rlang,is_atomic) 44 | importFrom(rlang,is_missing) 45 | importFrom(rlang,is_na) 46 | importFrom(rlang,is_null) 47 | importFrom(rlang,names2) 48 | importFrom(stringi,stri_replace_all_regex) 49 | -------------------------------------------------------------------------------- /R/clean.R: -------------------------------------------------------------------------------- 1 | #' Clean Rust binaries and package cache. 2 | #' 3 | #' Removes Rust binaries (such as `.dll`/`.so` libraries), C wrapper object files, 4 | #' invokes `cargo clean` to reset cargo target directory 5 | #' (found by default at `pkg_root/src/rust/target/`). 6 | #' Useful when Rust code should be recompiled from scratch. 7 | #' 8 | #' @param path character scalar, path to R package root. 9 | #' @param echo logical scalar, should cargo command and outputs be printed to 10 | #' console (default is `TRUE`) 11 | #' 12 | #' @return character vector with names of all deleted files (invisibly). 13 | #' 14 | #' @export 15 | #' 16 | #' @examples 17 | #' \dontrun{ 18 | #' clean() 19 | #' } 20 | clean <- function(path = ".", echo = TRUE) { 21 | check_string(path, class = "rextendr_error") 22 | check_bool(echo, class = "rextendr_error") 23 | 24 | manifest_path <- find_extendr_manifest(path = path) 25 | 26 | # Note: This should be adjusted if `TARGET_DIR` changes in `Makevars` 27 | target_dir <- rprojroot::find_package_root_file( 28 | "src", "rust", "target", 29 | path = path 30 | ) 31 | 32 | if (!dir.exists(target_dir)) { 33 | cli::cli_abort( 34 | c( 35 | "Could not clean binaries.", 36 | "Target directory not found at {.path target_dir}." 37 | ), 38 | call = rlang::caller_call(), 39 | class = "rextendr_error" 40 | ) 41 | } 42 | 43 | args <- c( 44 | "clean", 45 | glue::glue("--manifest-path={manifest_path}"), 46 | glue::glue("--target-dir={target_dir}"), 47 | if (tty_has_colors()) { 48 | "--color=always" 49 | } else { 50 | "--color=never" 51 | } 52 | ) 53 | 54 | run_cargo( 55 | args, 56 | wd = find_extendr_crate(path = path), 57 | echo = echo 58 | ) 59 | 60 | root <- rprojroot::find_package_root_file(path = path) 61 | 62 | if (!dir.exists(root)) { 63 | cli::cli_abort( 64 | "Could not clean binaries.", 65 | "R package directory not found at {.path root}.", 66 | call = rlang::caller_call(), 67 | class = "rextendr_error" 68 | ) 69 | } 70 | 71 | pkgbuild::clean_dll(path = root) 72 | } 73 | -------------------------------------------------------------------------------- /R/cran-compliance.R: -------------------------------------------------------------------------------- 1 | #' Vendor Rust dependencies 2 | #' 3 | #' `vendor_pkgs()` is used to package the dependencies as required by CRAN. 4 | #' It executes `cargo vendor` on your behalf creating a `vendor/` directory and a 5 | #' compressed `vendor.tar.xz` which will be shipped with package itself. 6 | #' If you have modified your dependencies, you will need need to repackage 7 | # the vendored dependencies using [`vendor_pkgs()`]. 8 | #' 9 | #' @inheritParams use_extendr 10 | #' @returns 11 | #' 12 | #' - `vendor_pkgs()` returns a data.frame with two columns `crate` and `version` 13 | #' 14 | #' @examples 15 | #' \dontrun{ 16 | #' vendor_pkgs() 17 | #' } 18 | #' @export 19 | vendor_pkgs <- function(path = ".", quiet = FALSE, overwrite = NULL) { 20 | stderr_line_callback <- function(x, proc) { 21 | if (!cli::ansi_grepl("To use vendored sources", x) && cli::ansi_nzchar(x)) { 22 | cli::cat_bullet(stringi::stri_trim_left(x)) 23 | } 24 | } 25 | local_quiet_cli(quiet) 26 | 27 | # get path to rust folder 28 | src_dir <- rprojroot::find_package_root_file("src", "rust", path = path) 29 | 30 | # if `src/rust` does not exist error 31 | if (!dir.exists(src_dir)) { 32 | cli::cli_abort( 33 | "{.path src/rust} cannot be found. Did you run {.fn use_extendr}?", 34 | class = "rextendr_error" 35 | ) 36 | } 37 | 38 | # if cargo.lock does not exist, cerate it using `cargo update` 39 | cargo_lock_fp <- file.path(src_dir, "Cargo.lock") 40 | 41 | if (!file.exists(cargo_lock_fp)) { 42 | withr::with_dir(src_dir, { 43 | update_res <- processx::run( 44 | "cargo", 45 | c( 46 | "generate-lockfile", 47 | "--manifest-path", 48 | file.path(src_dir, "Cargo.toml") 49 | ), 50 | stderr_line_callback = stderr_line_callback 51 | ) 52 | }) 53 | 54 | if (update_res[["status"]] != 0) { 55 | cli::cli_abort( 56 | "{.file Cargo.lock} could not be created using {.code cargo generate-lockfile}", 57 | class = "rextendr_error" 58 | ) 59 | } 60 | } 61 | 62 | # vendor crates 63 | withr::with_dir(src_dir, { 64 | vendor_res <- processx::run( 65 | "cargo", 66 | c( 67 | "vendor", 68 | "--locked", 69 | "--manifest-path", 70 | file.path(src_dir, "Cargo.toml") 71 | ), 72 | stderr_line_callback = stderr_line_callback 73 | ) 74 | }) 75 | 76 | if (vendor_res[["status"]] != 0) { 77 | cli::cli_abort( 78 | "{.code cargo vendor} failed", 79 | class = "rextendr_error" 80 | ) 81 | } 82 | 83 | # create a dataframe of vendored crates 84 | vendored <- vendor_res[["stderr"]] |> 85 | cli::ansi_strip() |> 86 | stringi::stri_split_lines1() 87 | 88 | res <- stringi::stri_match_first_regex(vendored, "Vendoring\\s([A-z0-9_][A-z0-9_-]*?)\\s[vV](.+?)(?=\\s)") |> 89 | as.data.frame() |> 90 | rlang::set_names(c("source", "crate", "version")) |> 91 | dplyr::filter(!is.na(source)) |> 92 | dplyr::select(-source) |> 93 | dplyr::arrange(.data$crate) 94 | 95 | # capture vendor-config.toml content 96 | config_toml <- vendor_res[["stdout"]] |> 97 | cli::ansi_strip() |> 98 | stringi::stri_split_lines1() 99 | 100 | # always write to file as cargo vendor catches things like patch.crates-io 101 | # and provides the appropriate configuration. 102 | brio::write_lines(config_toml, file.path(src_dir, "vendor-config.toml")) 103 | cli::cli_alert_info("Writing {.file src/rust/vendor-config.toml}") 104 | 105 | # compress to vendor.tar.xz 106 | compress_res <- withr::with_dir(src_dir, { 107 | processx::run( 108 | "tar", c( 109 | "-cJ", "--no-xattrs", "-f", "vendor.tar.xz", "vendor" 110 | ) 111 | ) 112 | }) 113 | 114 | if (compress_res[["status"]] != 0) { 115 | cli::cli_abort( 116 | "Folder {.path vendor} could not be compressed", 117 | class = "rextendr_error" 118 | ) 119 | } 120 | 121 | # return packages and versions invisibly 122 | invisible(res) 123 | } 124 | 125 | 126 | #' CRAN compliant extendr packages 127 | #' 128 | #' R packages developed using extendr are not immediately ready to 129 | #' be published to CRAN. The extendr package template ensures that 130 | #' CRAN publication is (farily) painless. 131 | #' 132 | #' @section CRAN requirements: 133 | #' 134 | #' In order to publish a Rust based package on CRAN it must meet certain 135 | #' requirements. These are: 136 | #' 137 | #' - Rust dependencies are vendored 138 | #' - The package is compiled offline 139 | #' - the `DESCRIPTION` file's `SystemRequirements` field contains `Cargo (Rust's package manager), rustc` 140 | #' 141 | #' The extendr templates handle all of this _except_ vendoring dependencies. 142 | #' This must be done prior to publication using [`vendor_pkgs()`]. 143 | #' 144 | #' In addition, it is important to make sure that CRAN maintainers 145 | #' are aware that the package they are checking contains Rust code. 146 | #' Depending on which and how many crates are used as a dependencies 147 | #' the `vendor.tar.xz` will be larger than a few megabytes. If a 148 | #' built package is larger than 5mbs CRAN may reject the submission. 149 | #' 150 | #' To prevent rejection make a note in your `cran-comments.md` file 151 | #' (create one using [`usethis::use_cran_comments()`]) along the lines of 152 | #' "The package tarball is 6mb because Rust dependencies are vendored within src/rust/vendor.tar.xz which is 5.9mb." 153 | #' @name cran 154 | NULL 155 | -------------------------------------------------------------------------------- /R/create_extendr_package.R: -------------------------------------------------------------------------------- 1 | #' Create package that uses Rust 2 | #' 3 | #' @description 4 | #' This function creates an R project directory for package development 5 | #' with Rust extensions. 6 | #' 7 | #' @inheritParams usethis::create_package 8 | #' @param ... arguments passed on to `usethis::create_package()` and 9 | #' `rextendr::use_extendr()` 10 | #' 11 | #' @return Path to the newly created project or package, invisibly. 12 | #' @keywords internal 13 | #' 14 | #' @noRd 15 | create_extendr_package <- function(path, ...) { 16 | # error if usethis is not installed 17 | rlang::check_installed("usethis") 18 | 19 | args <- rlang::list2(...) 20 | 21 | # hunch is that rstudio project text input widgets return empty strings 22 | # when no value is given, want to make sure it is NULL so `use_extendr()` 23 | # handles it correctly 24 | nullify_empty_string <- function(x) { 25 | if (rlang::is_string(x) && nzchar(x)) x else NULL 26 | } 27 | 28 | args <- map(args, nullify_empty_string) 29 | 30 | # build package directory, but don't start a new R session with 31 | # it as the working directory! i.e., set `open = FALSE` 32 | usethis::create_package( 33 | path, 34 | fields = list(), 35 | rstudio = TRUE, 36 | roxygen = args[["roxygen"]] %||% TRUE, 37 | check_name = args[["check_name"]] %||% TRUE, 38 | open = FALSE 39 | ) 40 | 41 | # add rust scaffolding to project dir 42 | use_extendr( 43 | path, 44 | crate_name = args[["crate_name"]], 45 | lib_name = args[["lib_name"]], 46 | quiet = TRUE, 47 | overwrite = TRUE, 48 | edition = args[["edition"]] %||% TRUE 49 | ) 50 | 51 | invisible(path) 52 | } 53 | -------------------------------------------------------------------------------- /R/eval.R: -------------------------------------------------------------------------------- 1 | #' Evaluate Rust code 2 | #' 3 | #' Compile and evaluate one or more Rust expressions. If the last 4 | #' expression in the Rust code returns a value (i.e., does not end with 5 | #' `;`), then this value is returned to R. The value returned does not need 6 | #' to be of type `Robj`, as long as it can be cast into this type with 7 | #' `.into()`. This conversion is done automatically, so you don't have to 8 | #' worry about it in your code. 9 | #' @param code Input rust code. 10 | #' @param env The R environment in which the Rust code will be evaluated. 11 | #' @param ... Other parameters handed off to [rust_function()]. 12 | #' @return The return value generated by the Rust code. 13 | #' @examples 14 | #' \dontrun{ 15 | #' # Rust code without return value, called only for its side effects 16 | #' rust_eval( 17 | #' code = 'rprintln!("hello from Rust!");' 18 | #' ) 19 | #' 20 | #' # Rust code with return value 21 | #' rust_eval( 22 | #' code = " 23 | #' let x = 5; 24 | #' let y = 7; 25 | #' let z = x * y; 26 | #' z // return to R; rust_eval() takes care of type conversion code 27 | #' " 28 | #' ) 29 | #' } 30 | #' @export 31 | rust_eval <- function(code, env = parent.frame(), ...) { 32 | rust_eval_deferred(code = code, env = env, ...)() 33 | } 34 | 35 | #' Evaluate Rust code (deferred) 36 | #' 37 | #' Compiles a chunk of Rust code and returns an R function, 38 | #' which, when called, executes Rust code. 39 | #' This allows to separate Rust code compilation and execution. 40 | #' The function can be called only once, it cleans up resources on exit, 41 | #' including loaded dll and sourced R wrapper. 42 | #' 43 | #' @inheritParams rust_eval 44 | #' @return \[`function()`\] An R function with no argumetns. 45 | #' @noRd 46 | rust_eval_deferred <- function(code, env = parent.frame(), ...) { 47 | # make sure code is given as a single character string 48 | code <- glue_collapse(code, sep = "\n") 49 | 50 | # Snippet hash is constructed from the Rust source code and 51 | # a unique identifier of the compiled dll. 52 | # Every time any rust code is dynamically compiled, 53 | # `the$count` is incremented. 54 | # This ensures that any two (even bytewise-identical) 55 | # Rust source code strings will have different 56 | # hashes. 57 | snippet_hash <- rlang::hash(list(the$count, code)) # nolint: object_usage_linter 58 | 59 | # The unique hash is then used to generate unique function names 60 | fn_name <- glue("rextendr_rust_eval_fun_{snippet_hash}") 61 | 62 | # wrap code into Rust function 63 | code_wrapped <- glue(r"( 64 | fn {fn_name}() -> Result {{ 65 | let x = {{ 66 | {code} 67 | }}; 68 | Ok(x.into()) 69 | }} 70 | )") 71 | 72 | # Attempt to figure out whether the Rust code returns a result or not, 73 | # and make the result invisible or not accordingly. This regex approach 74 | # is not perfect, but since it only affects the visibility of the result 75 | # that's Ok. Worst case scenario a result that should be invisible is 76 | # shown as visible. 77 | has_no_return <- grepl(".*;\\s*$", code, perl = TRUE) 78 | 79 | out <- rust_function(code = code_wrapped, env = env, ...) 80 | 81 | generated_fn <- function() { 82 | fn_handle <- get0(fn_name, envir = env, ifnotfound = NULL) 83 | dll_handle <- find_loaded_dll(out[["name"]]) 84 | if ( 85 | rlang::is_null(fn_handle) || 86 | rlang::is_null(dll_handle) 87 | ) { 88 | cli::cli_abort( 89 | c( 90 | "The Rust code fragment is no longer available for execution.", 91 | "i" = "Code fragment can only be executed once.", 92 | "!" = "Make sure you are not re-using an outdated fragment." 93 | ), 94 | class = "rextendr_error" 95 | ) 96 | } 97 | 98 | withr::defer(dyn.unload(out[["path"]])) 99 | withr::defer(rm(list = fn_name, envir = env)) 100 | 101 | result <- rlang::exec(fn_name, .env = env) 102 | if (has_no_return) { 103 | invisible(result) 104 | } else { 105 | result 106 | } 107 | } 108 | 109 | attr(generated_fn, "function_name") <- fn_name 110 | attr(generated_fn, "dll_path") <- out[["path"]] 111 | 112 | generated_fn 113 | } 114 | 115 | 116 | #' Find loaded dll by name 117 | #' @param name \[`string`\] Name of the dll (as returned by `dyn.load(...)[["name"]]`). 118 | #' @return \[`DllInfo`|`NULL`\] An object representing a loaded dll or 119 | #' `NULL` if no such dll is loaded. 120 | #' @noRd 121 | find_loaded_dll <- function(name) { 122 | dlls <- keep(getLoadedDLLs(), \(.x) .x[["name"]] == name) 123 | if (rlang::is_empty(dlls)) { 124 | NULL 125 | } else { 126 | dlls[[1]] 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /R/features.R: -------------------------------------------------------------------------------- 1 | features_config <- rlang::env( 2 | known_features = c("ndarray", "faer", "serde", "either", "num-complex", "graphics") 3 | ) 4 | 5 | validate_extendr_features <- function(features, suppress_warnings) { 6 | features <- features %||% character(0) 7 | 8 | if (!vctrs::vec_is(features, character())) { 9 | cli::cli_abort( 10 | c( 11 | "!" = "{.arg features} expected to be a vector of type {.cls character}, but got {.cls {class(features)}}." 12 | ), 13 | class = "rextendr_error" 14 | ) 15 | } 16 | 17 | features <- unique(features) 18 | 19 | unknown_features <- features |> 20 | setdiff(features_config$known_features) |> 21 | discard_empty() 22 | 23 | if (!isTRUE(suppress_warnings) && length(unknown_features) > 0) { 24 | # alerts are to be short 1 liners 25 | # these are called separately 26 | cli::cli_warn( 27 | c( 28 | "Found unknown {.code extendr} feature{?s}: {.val {unknown_features}}.", 29 | "i" = inf_dev_extendr_used() 30 | ) 31 | ) # nolint: object_usage_linter 32 | } 33 | 34 | features 35 | } 36 | 37 | discard_empty <- function(input) { 38 | vctrs::vec_slice(input, nzchar(input)) 39 | } 40 | 41 | enable_features <- function(extendr_deps, features) { 42 | features <- setdiff(features, "graphics") 43 | if (length(features) == 0L) { 44 | return(extendr_deps) 45 | } 46 | 47 | extendr_api <- extendr_deps[["extendr-api"]] 48 | if (is.null(extendr_api)) { 49 | cli::cli_abort( 50 | "{.arg extendr_deps} should contain a reference to {.code extendr-api} crate.", 51 | class = "rextendr_error" 52 | ) 53 | } 54 | 55 | if (is.character(extendr_api)) { 56 | extendr_api <- list(version = extendr_api, features = array(features)) 57 | } else if (is.list(extendr_api)) { 58 | existing_features <- extendr_api[["features"]] %||% character(0) 59 | extendr_api[["features"]] <- array(unique(c(existing_features, features))) 60 | } else { 61 | cli::cli_abort( 62 | "{.arg extendr_deps} contains an invalid reference to {.code extendr-api} crate.", 63 | class = "rextendr_error" 64 | ) 65 | } 66 | 67 | extendr_deps[["extendr-api"]] <- extendr_api 68 | 69 | extendr_deps 70 | } 71 | -------------------------------------------------------------------------------- /R/find_exports.R: -------------------------------------------------------------------------------- 1 | find_exports <- function(clean_lns) { 2 | ids <- find_extendr_attrs_ids(clean_lns) 3 | start <- ids 4 | end <- dplyr::lead(ids, default = length(clean_lns) + 1L) - 1L 5 | 6 | # start and end may empty 7 | if (rlang::is_empty(start) || rlang::is_empty(end)) { 8 | return(data.frame(name = character(0), type = character(0), lifetime = character(0))) 9 | } 10 | 11 | map2(start, end, \(.x, .y) extract_meta(clean_lns[.x:.y])) |> 12 | discard(\(.x) is.na(.x["impl"]) & is.na(.x["fn"])) |> 13 | dplyr::bind_rows() |> 14 | dplyr::mutate(type = dplyr::coalesce(.data$impl, .data$fn)) |> 15 | dplyr::select(dplyr::all_of(c("name", "type", "lifetime"))) 16 | } 17 | 18 | # Finds lines which contain #[extendr] (allowing additional spaces) 19 | find_extendr_attrs_ids <- function(lns) { 20 | which(stringi::stri_detect_regex(lns, r"{#\s*\[\s*extendr(\s*\(.*\))?\s*\]}")) 21 | } 22 | 23 | # Gets function/module metadata from a subset of lines. 24 | # Finds first occurrence of `fn` or `impl`. 25 | extract_meta <- function(lns) { 26 | # Matches fn|impl<'a> item_name 27 | result <- stringi::stri_match_first_regex( 28 | glue_collapse(lns, sep = "\n"), 29 | "(?:(?struct)|(?enum)|(?fn)|(?impl)(?:\\s*<(?.+?)>)?)\\s+(?(?:r#)?(?:_\\w+|[A-z]\\w*))" # nolint: line_length_linter 30 | ) |> 31 | as.data.frame() |> 32 | rlang::set_names(c("match", "struct", "enum", "fn", "impl", "lifetime", "name")) |> 33 | dplyr::filter(!is.na(.data$match)) 34 | 35 | # If no matches have been found, then the attribute is misplaced 36 | if (nrow(result) == 0L) { 37 | # This unfortunately does not provide 38 | # meaningful output or source line numbers. 39 | code_sample <- stringi::stri_sub( 40 | glue_collapse(lns, sep = "\n "), 41 | 1, 80 42 | ) 43 | 44 | 45 | rlang::abort( 46 | cli::cli_fmt({ 47 | cli::cli_text( 48 | "Rust code contains invalid attribute macros." 49 | ) 50 | cli::cli_alert_danger( 51 | "No valid {.code fn} or {.code impl} block found in the \\ 52 | following sample:" 53 | ) 54 | cli::cli_code(code_sample) 55 | }), 56 | class = "rextendr_error" 57 | ) 58 | } 59 | result 60 | } 61 | -------------------------------------------------------------------------------- /R/find_extendr.R: -------------------------------------------------------------------------------- 1 | #' Get path to Rust crate in R package directory 2 | #' 3 | #' @param path character scalar, the R package directory 4 | #' @param error_call call scalar, from rlang docs: "the defused call with which 5 | #' the function running in the frame was invoked" 6 | #' 7 | #' @return character scalar, path to Rust crate 8 | #' 9 | #' @keywords internal 10 | #' @noRd 11 | find_extendr_crate <- function( 12 | path = ".", 13 | error_call = rlang::caller_call()) { 14 | check_character(path, call = error_call, class = "rextendr_error") 15 | 16 | rust_folder <- rprojroot::find_package_root_file( 17 | "src", "rust", 18 | path = path 19 | ) 20 | 21 | if (!dir.exists(rust_folder)) { 22 | cli::cli_abort( 23 | "Could not find Rust crate at {.path rust_folder}.", 24 | call = error_call, 25 | class = "rextendr_error" 26 | ) 27 | } 28 | 29 | rust_folder 30 | } 31 | 32 | #' Get path to Cargo manifest in R package directory 33 | #' 34 | #' @param path character scalar, the R package directory 35 | #' @param error_call call scalar, from rlang docs: "the defused call with which 36 | #' the function running in the frame was invoked" 37 | #' 38 | #' @return character scalar, path to Cargo manifest 39 | #' 40 | #' @keywords internal 41 | #' @noRd 42 | find_extendr_manifest <- function( 43 | path = ".", 44 | error_call = rlang::caller_call()) { 45 | check_character(path, call = error_call, class = "rextendr_error") 46 | 47 | manifest_path <- rprojroot::find_package_root_file( 48 | "src", "rust", "Cargo.toml", 49 | path = path 50 | ) 51 | 52 | if (!file.exists(manifest_path)) { 53 | cli::cli_abort( 54 | "Could not find Cargo manifest at {.path manifest_path}.", 55 | call = error_call, 56 | class = "rextendr_error" 57 | ) 58 | } 59 | 60 | manifest_path 61 | } 62 | -------------------------------------------------------------------------------- /R/function_options.R: -------------------------------------------------------------------------------- 1 | extendr_function_config <- rlang::env( 2 | known_options = data.frame( 3 | Name = c("r_name", "mod_name", "use_rng"), 4 | Ptype = I(list( 5 | character(), 6 | character(), 7 | logical() 8 | )) 9 | ) 10 | ) 11 | 12 | #' Converts a list of user-specified options into a data frame containing `Name` and `RustValue` 13 | #' 14 | #' @param options A list of user-specified options. 15 | #' @param suppress_warnings Logical, suppresses warnings if `TRUE`. 16 | #' @noRd 17 | convert_function_options <- function(options, suppress_warnings) { 18 | if (rlang::is_null(options) || rlang::is_empty(options)) { 19 | return(data.frame(Name = character(), RustValue = character())) 20 | } 21 | 22 | if (!rlang::is_list(options) || !rlang::is_named(options)) { 23 | cli::cli_abort( 24 | "Extendr function options should be either a named {.code list()} or {.code NULL}.", 25 | class = "rextendr_error" 26 | ) 27 | } 28 | 29 | options_table <- data.frame(Name = rlang::names2(options), Value = I(unname(options))) |> 30 | dplyr::left_join(extendr_function_config$known_options, by = "Name") |> 31 | dplyr::mutate( 32 | Value = pmap( 33 | list(.data$Value, .data$Ptype, .data$Name), 34 | \(...) if (rlang::is_null(..2)) ..1 else vctrs::vec_cast(..1, ..2, x_arg = ..3) 35 | ), 36 | ) 37 | 38 | unknown_option_names <- options_table |> 39 | dplyr::filter(map_lgl(.data$Ptype, rlang::is_null)) |> 40 | dplyr::pull(.data$Name) 41 | 42 | invalid_options <- options_table |> 43 | dplyr::mutate( 44 | IsNameInvalid = !is_valid_rust_name(.data$Name), 45 | IsValueNull = map_lgl(.data$Value, rlang::is_null), 46 | IsNotScalar = !.data$IsValueNull & !map_lgl(.data$Value, vctrs::vec_is, size = 1L) 47 | ) |> 48 | dplyr::filter( 49 | .data$IsNameInvalid | .data$IsValueNull | .data$IsNotScalar 50 | ) 51 | 52 | if (vctrs::vec_size(invalid_options) > 0) { 53 | cli_abort_invalid_options(invalid_options) 54 | } else if (!isTRUE(suppress_warnings) && length(unknown_option_names) > 0) { 55 | cli::cli_warn(c( 56 | "Found unknown {.code extendr} function option{?s}: {.val {unknown_option_names}}.", 57 | "i" = inf_dev_extendr_used() 58 | )) 59 | } 60 | 61 | options_table |> 62 | dplyr::transmute( 63 | .data$Name, 64 | RustValue = map_chr(.data$Value, convert_option_to_rust) 65 | ) 66 | } 67 | 68 | #' Throws an error given a data frame of invalid options 69 | #' 70 | #' @param invalid_options A data frame of invalid options. 71 | #' @noRd 72 | cli_abort_invalid_options <- function(invalid_options) { 73 | n_invalid_opts <- vctrs::vec_size(invalid_options) # nolint: object_usage_linter 74 | 75 | invalid_names <- invalid_options |> get_option_names(.data$IsNameInvalid) 76 | null_values <- invalid_options |> get_option_names(.data$IsValueNull) 77 | vector_values <- invalid_options |> get_option_names(.data$IsNotScalar) 78 | 79 | message <- c( 80 | "Found {.val {n_invalid_opts}} invalid {.code extendr} function option{?s}:", 81 | x = "Unsupported name{?s}: {.val {invalid_names}}." |> if_any_opts(invalid_names), 82 | x = "Null value{?s}: {.val {null_values}}." |> if_any_opts(null_values), 83 | x = "Vector value{?s}: {.val {vector_values}}." |> if_any_opts(vector_values), 84 | i = "Option names should be valid rust names." |> if_any_opts(invalid_names), 85 | i = "{.code NULL} values are disallowed." |> if_any_opts(null_values), 86 | i = "Only scalars are allowed as option values." |> if_any_opts(vector_values) 87 | ) 88 | 89 | cli::cli_abort(message, class = "rextendr_error") 90 | } 91 | 92 | #' Returns the names of options that satisfy the given filter 93 | #' @param invalid_options A data frame of invalid options. 94 | #' @param filter_column A column expression/name in the data frame. 95 | #' @return A character vector of option names. 96 | #' @noRd 97 | get_option_names <- function(invalid_options, filter_column) { 98 | invalid_options |> 99 | dplyr::filter({{ filter_column }}) |> 100 | dplyr::pull(.data$Name) 101 | } 102 | 103 | #' Returns the given text if the options are not empty 104 | #' @param text A string. 105 | #' @param options A character vector which length is tested. 106 | #' @return The given string if the options are not empty, otherwise an empty character vector 107 | #' @noRd 108 | if_any_opts <- function(text, options) { 109 | if (vctrs::vec_size(options) > 0) { 110 | text 111 | } else { 112 | character(0) 113 | } 114 | } 115 | 116 | #' Converts an R option value to a Rust option value 117 | #' 118 | #' @param option_value An R scalar option value. 119 | #' @return A Rust option value as a string. 120 | #' @noRd 121 | convert_option_to_rust <- function(option_value) { 122 | if (vctrs::vec_is(option_value, character())) { 123 | paste0("\"", option_value, "\"") 124 | } else if (vctrs::vec_is(option_value, logical())) { 125 | ifelse(option_value, "true", "false") 126 | } else { 127 | as.character(option_value) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /R/generate_toml.R: -------------------------------------------------------------------------------- 1 | generate_cargo.toml <- function(libname = "rextendr", 2 | dependencies = NULL, 3 | patch.crates_io = NULL, 4 | extendr_deps = NULL, 5 | features = character(0)) { 6 | 7 | # create an empty list if no dependencies are provided 8 | deps <- dependencies %||% list() 9 | # enabled extendr features that we need to impute into all of the 10 | # dependencies 11 | to_impute <- enable_features(extendr_deps, features) 12 | 13 | for (.name in names(to_impute)) { 14 | deps[[.name]] <- to_impute[[.name]] 15 | } 16 | 17 | to_toml( 18 | package = list( 19 | name = libname, 20 | version = "0.0.1", 21 | edition = "2021", 22 | resolver = "2" 23 | ), 24 | lib = list( 25 | `crate-type` = array("cdylib", 1) 26 | ), 27 | dependencies = deps, 28 | `patch.crates-io` = patch.crates_io, 29 | `profile.perf` = list( 30 | inherits = "release", 31 | lto = "thin", 32 | `opt-level` = 3, 33 | panic = "abort", 34 | `codegen-units` = 1 35 | ) 36 | ) 37 | } 38 | 39 | generate_cargo_config.toml <- function() { 40 | to_toml( 41 | build = list( 42 | rustflags = c("-C", "target-cpu=native"), 43 | `target-dir` = "target" 44 | ) 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /R/helpers.r: -------------------------------------------------------------------------------- 1 | tty_has_colors <- function() isTRUE(cli::num_ansi_colors() > 1L) 2 | 3 | get_cargo_envvars <- function() { 4 | if (identical(.Platform$OS.type, "windows")) { 5 | # On Windows, PATH to Rust toolchain should be set by the installer. 6 | # If R >= 4.2, we need to override the linker setting. 7 | if (identical(R.version$crt, "ucrt")) { 8 | # `rustc` adds `-lgcc_eh` flags to the compiler, but Rtools' GCC doesn't have 9 | # `libgcc_eh` due to the compilation settings. So, in order to please the 10 | # compiler, we need to add empty `libgcc_eh` to the library search paths. 11 | # 12 | # For more details, please refer to 13 | # https://github.com/r-windows/rtools-packages/blob/2407b23f1e0925bbb20a4162c963600105236318/mingw-w64-gcc/PKGBUILD#L313-L316 # nolint: line_length_linter 14 | libgcc_path <- file.path(system.file(package = "rextendr"), "libgcc_mock") 15 | dir.create(libgcc_path, showWarnings = FALSE) 16 | file.create(file.path(libgcc_path, "libgcc_eh.a")) 17 | 18 | cargo_envvars <- c("current", 19 | CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER = "x86_64-w64-mingw32.static.posix-gcc.exe", 20 | LIBRARY_PATH = paste0(libgcc_path, ";", Sys.getenv("LIBRARY_PATH")) 21 | ) 22 | } else { 23 | cargo_envvars <- NULL 24 | } 25 | } else { 26 | # In some environments, ~/.cargo/bin might not be included in PATH, so we need 27 | # to set it here to ensure cargo can be invoked. It's added to the tail as a 28 | # fallback, which is used only when cargo is not found in the user's PATH. 29 | path_envvar <- Sys.getenv("PATH", unset = "") # nolint: object_usage_linter 30 | cargo_path <- path.expand("~/.cargo/bin") # nolint: object_usage_linter 31 | # "current" means appending or overwriting the envvars in addition to the current ones. 32 | cargo_envvars <- c("current", PATH = glue("{path_envvar}:{cargo_path}")) 33 | } 34 | cargo_envvars 35 | } 36 | -------------------------------------------------------------------------------- /R/knitr_engine.R: -------------------------------------------------------------------------------- 1 | #' Knitr engines 2 | #' 3 | #' Two knitr engines that enable code chunks of type `extendr` (individual Rust 4 | #' statements to be evaluated via [rust_eval()]) and `extendrsrc` (Rust functions 5 | #' or classes that will be exported to R via [rust_source()]). 6 | #' @param options A list of chunk options. 7 | #' @return A character string representing the engine output. 8 | #' @export 9 | eng_extendr <- function(options) { 10 | eng_impl(options, rust_eval_deferred) 11 | } 12 | 13 | #' @rdname eng_extendr 14 | #' @export 15 | eng_extendrsrc <- function(options) { 16 | eng_impl(options, rust_source) 17 | } 18 | 19 | 20 | 21 | eng_impl <- function(options, extendr_engine) { 22 | if (!requireNamespace("knitr", quietly = TRUE)) { 23 | cli::cli_abort( 24 | "The {.pkg knitr} package is required to run the extendr chunk engine.", 25 | class = "rextendr_error" 26 | ) 27 | } 28 | 29 | if (!is.null(options$preamble)) { 30 | code <- c( 31 | lapply(options$preamble, function(x) knitr::knit_code$get(x)), 32 | recursive = TRUE 33 | ) 34 | code <- c(code, options$code) 35 | } else { 36 | code <- options$code 37 | } 38 | 39 | code <- glue_collapse(code, sep = "\n") # code to compile 40 | code_out <- glue_collapse(options$code, sep = "\n") # code to output to html 41 | 42 | # engine.opts is a list of arguments to be passed to rust_eval, e.g. 43 | # engine.opts = list(dependencies = list(`pulldown-cmark` = "0.8")) 44 | opts <- options$engine.opts 45 | 46 | if (!is.environment(opts$env)) { 47 | # default env is knit_global() 48 | opts$env <- knitr::knit_global() 49 | } 50 | 51 | cli::cli_alert_success("Compiling Rust extendr code chunk...") 52 | compiled_code <- do.call(extendr_engine, c(list(code = code), opts)) 53 | 54 | if (isTRUE(options$eval) && rlang::is_function(compiled_code)) { 55 | cli::cli_alert_success("Evaluating Rust extendr code chunk...") 56 | 57 | out <- utils::capture.output({ 58 | result <- withVisible( 59 | compiled_code() 60 | ) 61 | if (isTRUE(result$visible)) { 62 | print(result$value) 63 | } 64 | }) 65 | } else { 66 | out <- "" 67 | } 68 | 69 | options$engine <- "rust" # wrap up source code in rust syntax 70 | knitr::engine_output(options, code_out, out) 71 | } 72 | -------------------------------------------------------------------------------- /R/license_note.R: -------------------------------------------------------------------------------- 1 | #' Generate LICENSE.note file. 2 | #' 3 | #' LICENSE.note generated by this function contains information about all 4 | #' recursive dependencies in Rust crate. 5 | #' 6 | #' @param path character scalar, the R package directory 7 | #' @param quiet logical scalar, whether to signal successful writing of 8 | #' LICENSE.note (default is `FALSE`) 9 | #' @param force logical scalar, whether to regenerate LICENSE.note if 10 | #' LICENSE.note already exists (default is `TRUE`) 11 | #' 12 | #' @return text printed to LICENSE.note (invisibly). 13 | #' 14 | #' @export 15 | #' 16 | #' @examples 17 | #' \dontrun{ 18 | #' write_license_note() 19 | #' } 20 | write_license_note <- function( 21 | path = ".", 22 | quiet = FALSE, 23 | force = TRUE) { 24 | check_string(path, class = "rextendr_error") 25 | check_bool(quiet, class = "rextendr_error") 26 | check_bool(force, class = "rextendr_error") 27 | 28 | outfile <- rprojroot::find_package_root_file( 29 | "LICENSE.note", 30 | path = path 31 | ) 32 | 33 | args <- c( 34 | "metadata", 35 | "--format-version=1" 36 | ) 37 | 38 | metadata <- run_cargo( 39 | args, 40 | wd = find_extendr_crate(path = path), 41 | echo = FALSE, 42 | parse_json = TRUE 43 | ) 44 | 45 | packages <- metadata[["packages"]] 46 | 47 | # did we actually get the recursive dependency metadata we need? 48 | required_variables <- c("name", "repository", "authors", "license", "id") 49 | 50 | packages_exist <- is.data.frame(packages) && 51 | !is.null(packages) && 52 | nrow(packages) > 0 && 53 | all(required_variables %in% names(packages)) 54 | 55 | if (!packages_exist) { 56 | cli::cli_abort( 57 | "Unable to write LICENSE.note.", 58 | "Metadata for recursive dependencies not found.", 59 | call = rlang::caller_call(), 60 | class = "rextendr_error" 61 | ) 62 | } 63 | 64 | # exclude current package from LICENSE.note 65 | current_package <- metadata[["resolve"]][["root"]] 66 | 67 | current_package_exists <- length(current_package) == 1 && 68 | is.character(current_package) && 69 | !is.null(current_package) 70 | 71 | if (!current_package_exists) { 72 | cli::cli_abort( 73 | "Unable to write LICENSE.note.", 74 | "Failed to identify current Rust crate.", 75 | call = rlang::caller_call(), 76 | class = "rextendr_error" 77 | ) 78 | } 79 | 80 | packages <- packages[packages[["id"]] != current_package, ] 81 | 82 | # replace missing values 83 | packages[["respository"]] <- replace_na( 84 | packages[["repository"]], 85 | "unknown" 86 | ) 87 | 88 | packages[["licenses"]] <- replace_na( 89 | packages[["repository"]], 90 | "not provided" 91 | ) 92 | 93 | # remove email addresses and special characters and combine all authors 94 | # of a crate into a single character scalar 95 | packages[["authors"]] <- unlist(Map( 96 | prep_authors, 97 | packages[["authors"]], 98 | packages[["name"]] 99 | )) 100 | 101 | separator <- "-------------------------------------------------------------" 102 | 103 | note_header <- paste0( 104 | "The binary compiled from the source code of this package ", 105 | "contains the following Rust crates:\n", 106 | "\n", 107 | "\n", 108 | separator 109 | ) 110 | 111 | note_body <- paste0( 112 | "\n", 113 | "Name: ", packages[["name"]], "\n", 114 | "Repository: ", packages[["repository"]], "\n", 115 | "Authors: ", packages[["authors"]], "\n", 116 | "License: ", packages[["license"]], "\n", 117 | "\n", 118 | separator 119 | ) 120 | 121 | write_file( 122 | text = c(note_header, note_body), 123 | path = outfile, 124 | search_root_from = path, 125 | quiet = quiet, 126 | overwrite = force 127 | ) 128 | } 129 | 130 | prep_authors <- function(authors, package) { 131 | authors <- ifelse( 132 | is.na(authors), 133 | paste0(package, " authors"), 134 | authors 135 | ) 136 | 137 | authors <- stringi::stri_replace_all_regex(authors, r"(\ <.+?>)", "") 138 | 139 | paste0(authors, collapse = ", ") 140 | } 141 | -------------------------------------------------------------------------------- /R/make_module_macro.R: -------------------------------------------------------------------------------- 1 | #' Generate extendr module macro for Rust source 2 | #' 3 | #' Read some Rust source code, find functions or implementations with the 4 | #' `#[extendr]` attribute, and generate an `extendr_module!` macro statement. 5 | #' 6 | #' This function uses simple regular expressions to do the Rust parsing and 7 | #' can get confused by valid Rust code. It is only meant as a convenience for 8 | #' simple use cases. In particular, it cannot currently handle implementations 9 | #' for generics. 10 | #' @param code Character vector containing Rust code. 11 | #' @param module_name Module name 12 | #' @return Character vector holding the contents of the generated macro statement. 13 | #' @keywords internal 14 | #' @export 15 | make_module_macro <- function(code, module_name = "rextendr") { 16 | # make sure we have cleanly separated lines 17 | lines <- stringi::stri_split_lines( 18 | glue_collapse(code, sep = "\n"), 19 | omit_empty = TRUE 20 | )[[1]] 21 | 22 | idents <- find_exports(sanitize_rust_code(lines)) 23 | outlines <- c("extendr_module! {", glue("mod {module_name};")) 24 | outlines <- c(outlines, glue::glue_data(idents, "{type} {name};")) 25 | outlines <- c(outlines, "}") 26 | outlines 27 | } 28 | -------------------------------------------------------------------------------- /R/read_cargo_metadata.R: -------------------------------------------------------------------------------- 1 | #' Retrieve metadata for packages and workspaces 2 | #' 3 | #' @param path character scalar, the R package directory 4 | #' @param dependencies Default `FALSE`. A logical scalar, whether to include 5 | #' all recursive dependencies in stdout. 6 | #' @param echo Default `FALSE`. A logical scalar, should cargo command and 7 | #' outputs be printed to the console. 8 | #' 9 | #' @details 10 | #' For more details, see 11 | #' \href{https://doc.rust-lang.org/cargo/commands/cargo-metadata.html}{Cargo docs} 12 | #' for `cargo-metadata`. See especially "JSON Format" to get a sense of what you 13 | #' can expect to find in the returned list. 14 | #' 15 | #' @returns 16 | #' A `list` including the following elements: 17 | #' - `packages` 18 | #' - `workspace_members` 19 | #' - `workspace_default_members` 20 | #' - `resolve` 21 | #' - `target_directory` 22 | #' - `version` 23 | #' - `workspace_root` 24 | #' - `metadata` 25 | #' 26 | #' @export 27 | #' 28 | #' @examples 29 | #' \dontrun{ 30 | #' read_cargo_metadata() 31 | #' } 32 | #' 33 | read_cargo_metadata <- function( 34 | path = ".", 35 | dependencies = FALSE, 36 | echo = FALSE) { 37 | check_string(path, class = "rextendr_error") 38 | check_bool(dependencies, class = "rextendr_error") 39 | check_bool(echo, class = "rextendr_error") 40 | 41 | args <- c( 42 | "metadata", 43 | "--format-version=1", 44 | if (!dependencies) { 45 | "--no-deps" 46 | }, 47 | if (tty_has_colors()) { 48 | "--color=always" 49 | } else { 50 | "--color=never" 51 | } 52 | ) 53 | 54 | run_cargo( 55 | args, 56 | wd = find_extendr_crate(path = path), 57 | echo = echo, 58 | parse_json = TRUE 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /R/rextendr.R: -------------------------------------------------------------------------------- 1 | #' Call Rust code from R using the 'extendr' Crate 2 | #' 3 | #' The rextendr package implements functions to interface with Rust code from R. 4 | #' See [rust_source()] for details. 5 | #' @name rextendr 6 | #' @keywords internal 7 | "_PACKAGE" 8 | 9 | #' @importFrom dplyr mutate 10 | #' @importFrom glue glue glue_collapse 11 | #' @importFrom rlang dots_list names2 as_function is_missing is_atomic is_null 12 | #' @importFrom rlang is_na .data .env caller_env as_name as_label enquo %||% 13 | #' @importFrom stringi stri_replace_all_regex 14 | NULL 15 | -------------------------------------------------------------------------------- /R/rextendr_document.R: -------------------------------------------------------------------------------- 1 | #' Compile Rust code and generate package documentation. 2 | #' 3 | #' The function `rextendr::document()` updates the package documentation for an 4 | #' R package that uses `extendr` code, taking into account any changes that were 5 | #' made in the Rust code. It is a wrapper for [devtools::document()], and it 6 | #' executes `extendr`-specific routines before calling [devtools::document()]. 7 | #' Specifically, it ensures that Rust code is recompiled (when necessary) and that 8 | #' up-to-date R wrappers are generated before regenerating the package documentation. 9 | #' @inheritParams devtools::document 10 | #' @return No return value, called for side effects. 11 | #' @export 12 | document <- function(pkg = ".", quiet = FALSE, roclets = NULL) { 13 | withr::local_envvar(devtools::r_env_vars()) 14 | 15 | register_extendr(path = pkg, quiet = quiet) 16 | 17 | rlang::check_installed("devtools") 18 | devtools::document(pkg = pkg, roclets = roclets, quiet = quiet) 19 | if (!isTRUE(quiet)) { 20 | check_namespace_file(pkg) 21 | } 22 | } 23 | 24 | check_if_roxygen_used <- function(namespace_content) { 25 | any(stringi::stri_startswith_fixed(namespace_content, "# Generated by roxygen2:")) 26 | } 27 | 28 | check_if_dyn_lib_used <- function(namespace_content, pkg_name) { 29 | expected_pattern <- glue::glue("useDynLib\\({pkg_name},\\s*\\.registration = TRUE\\)") 30 | 31 | any(stringi::stri_detect_regex(namespace_content, expected_pattern)) 32 | } 33 | 34 | check_namespace_file <- function(path = ".") { 35 | namespace_file_path <- rprojroot::find_package_root_file("NAMESPACE", path = path) 36 | description_file_path <- rprojroot::find_package_root_file("DESCRIPTION", path = path) 37 | package_name <- desc::desc_get_field("Package", file = description_file_path) 38 | namespace_content <- brio::read_lines(namespace_file_path) 39 | namespace_content <- stringi::stri_trim_both(namespace_content[nzchar(namespace_content)]) 40 | 41 | is_roxygen_used <- check_if_roxygen_used(namespace_content) 42 | is_dyn_lib_used <- check_if_dyn_lib_used(namespace_content, package_name) 43 | 44 | if (!is_dyn_lib_used) { 45 | roxygen_message <- ifelse(is_roxygen_used, NULL, 46 | paste( 47 | "Alternatively, allow {.pkg roxygen2} to generate {.file NAMESPACE} exports for you.", 48 | "Annotate exported functions with {.code # @export} directive,", 49 | "delete the {.file NAMESPACE} file and run {.code rextendr::document()} again to regenerate exports." 50 | ) 51 | ) 52 | 53 | use_dyn_lib_ref <- glue::glue("useDynLib({package_name}, .registration = TRUE)") # nolint: object_usage_linter. 54 | 55 | cli::cli_warn( 56 | c( 57 | "The {.file NAMESPACE} file does not contain the expected {.code useDynLib} directive.", 58 | "x" = "This prevents your package from loading Rust code.", 59 | "*" = "Add the following line to the {.file NAMESPACE} file: {.code {use_dyn_lib_ref}}", 60 | "*" = roxygen_message 61 | ) 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /R/run_cargo.R: -------------------------------------------------------------------------------- 1 | #' Run Cargo subcommands 2 | #' 3 | #' This internal function allows us to maintain consistent specifications for 4 | #' `processx::run()` everywhere it uses. 5 | #' 6 | #' @param args character vector, the Cargo subcommand and flags to be executed. 7 | #' @param wd character scalar, location of the Rust crate, (default is 8 | #' `find_extendr_crate()`). 9 | #' @param error_on_status Default `TRUE`. A logical scalar, whether to error on a non-zero exist status. 10 | #' @param echo_cmd Default `TRUE`. A logical scalar, whether to print Cargo subcommand and flags 11 | #' to the console. 12 | #' @param echo Default `TRUE`. Alogical scalar, whether to print standard output and error to the 13 | #' console. 14 | #' @param env character vector, environment variables of the child process. 15 | #' @param parse_json Default `FALSE`. A logical scalar, whether to parse JSON-structured standard 16 | #' output using [`jsonlite::parse_json()`] with `simplifyDataFrame = TRUE`. 17 | #' @param error_call Default [`rlang::caller_call()`]. The defused call with which 18 | #' the function running in the frame was invoked. 19 | #' @param ... additional arguments passed to [`processx::run()`]. 20 | #' @returns 21 | #' A list with elements `status`, `stdout`, `stderr`, and `timeout`. 22 | #' See [`processx::run()`]. If `parse_json = TRUE`, result of parsing 23 | #' JSON-structured standard output. 24 | #' 25 | #' @keywords internal 26 | #' @noRd 27 | run_cargo <- function( 28 | args, 29 | wd = find_extendr_crate(), 30 | error_on_status = TRUE, 31 | echo = TRUE, 32 | env = get_cargo_envvars(), 33 | parse_json = FALSE, 34 | error_call = rlang::caller_call(), 35 | ... 36 | ) { 37 | check_character(args, call = error_call, class = "rextendr_error") 38 | check_string(wd, call = error_call, class = "rextendr_error") 39 | check_bool(error_on_status, call = error_call, class = "rextendr_error") 40 | check_bool(echo, call = error_call, class = "rextendr_error") 41 | check_character(env, call = error_call, class = "rextendr_error") 42 | check_bool(parse_json, call = error_call, class = "rextendr_error") 43 | 44 | out <- processx::run( 45 | command = "cargo", 46 | args = args, 47 | error_on_status = error_on_status, 48 | wd = wd, 49 | echo_cmd = echo, 50 | echo = echo, 51 | env = env, 52 | ... 53 | ) 54 | 55 | stdout <- out[["stdout"]] 56 | 57 | if (length(stdout) != 1L || !is.character(stdout) || is.null(stdout)) { 58 | cli::cli_abort( 59 | "{.code cargo paste(args, collapse = ' ')} failed to return stdout.", 60 | call = error_call, 61 | class = "rextendr_error" 62 | ) 63 | } 64 | 65 | if (parse_json) { 66 | res <- rlang::try_fetch( 67 | jsonlite::parse_json(stdout, simplifyDataFrame = TRUE), 68 | error = function(cnd) { 69 | cli::cli_abort( 70 | c("Failed to {.code stdout} as json:", " " = "{stdout}"), 71 | parent = cnd, 72 | class = "rextendr_error" 73 | ) 74 | } 75 | ) 76 | return(res) 77 | } 78 | 79 | out 80 | } 81 | -------------------------------------------------------------------------------- /R/sanitize_code.R: -------------------------------------------------------------------------------- 1 | sanitize_rust_code <- function(lines) { 2 | lines |> 3 | remove_empty_or_whitespace() |> 4 | fill_block_comments() |> 5 | remove_line_comments() |> 6 | remove_empty_or_whitespace() 7 | } 8 | 9 | remove_empty_or_whitespace <- function(lns) { 10 | stringi::stri_subset_regex(lns, "^\\s*$", negate = TRUE) 11 | } 12 | 13 | remove_line_comments <- function(lns) { 14 | stringi::stri_replace_first_regex(lns, "//.*$", "") 15 | } 16 | 17 | # Because R does not allow straightforward iteration over 18 | # scalar strings, determining `/*` and `*/` positions can be challenging. 19 | # E.g., regex matches 3 `/*` and 3 `*/` in `/*/**/*/`. 20 | # 1. We find all occurrence of `/*` and `*/`. 21 | # 2. We find non-overlapping `/*` and `*/`. 22 | # 3. We build pairs of open-close comment delimiters by collapsing nested 23 | # comments. 24 | # 4. We fill in space between remaining delimiters with spaces (simplest way). 25 | fill_block_comments <- function(lns, fill_with = " ") { # nolint: object_usage_linter 26 | lns <- glue_collapse(lns, sep = "\n") 27 | 28 | # Fast path if character input is empty 29 | if (length(lns) == 0L || !nzchar(lns)) { 30 | return(character(0)) 31 | } 32 | 33 | locations <- stringi::stri_locate_all_regex(lns, c("/\\*", "\\*/")) 34 | 35 | # A sorted DF having `start`, `end`, and `type` 36 | comment_syms <- 37 | locations |> 38 | map(as.data.frame) |> 39 | imap( 40 | \(.x, .y) { 41 | dplyr::mutate( 42 | .x, 43 | type = dplyr::if_else(.y == 1L, "open", "close") 44 | ) 45 | } 46 | ) |> 47 | dplyr::bind_rows() |> 48 | dplyr::filter(!is.na(.data$start)) |> 49 | dplyr::arrange(.data$start) 50 | 51 | # Fast path if no comments are found at all. 52 | if ( 53 | all(is.na(comment_syms[["start"]])) && 54 | all(is.na(comment_syms[["end"]])) 55 | ) { 56 | return( 57 | stringi::stri_split_lines( 58 | lns, 59 | omit_empty = TRUE 60 | )[[1]] 61 | ) 62 | } 63 | n <- nrow(comment_syms) 64 | selects <- logical(n) 65 | selects[1:n] <- TRUE 66 | # Select non-overlapping delimiters, starting with 1st 67 | i <- 2L 68 | while (i <= n) { 69 | if (comment_syms[["start"]][i] == comment_syms[["end"]][i - 1L]) { 70 | # If current overlaps with previous, exclude current and 71 | # jump over the next one, which is included automatically. 72 | selects[i] <- FALSE 73 | i <- i + 1L 74 | } 75 | # `i` can be incremented twice per cycle, this is intentional. 76 | i <- i + 1L 77 | } 78 | 79 | # Contains only valid comment delimiters in order of appearance. 80 | valid_syms <- dplyr::slice(comment_syms, which(.env$selects)) 81 | 82 | n_open <- sum(valid_syms[["type"]] == "open") 83 | n_close <- sum(valid_syms[["type"]] == "close") 84 | # Fails if number of `/*` and `*/` are different. 85 | if (n_open != n_close) { 86 | cli::cli_abort( 87 | c( 88 | "Malformed comments.", 89 | "x" = "Number of start {.code /*} and end {.code */} \\ 90 | delimiters are not equal.", 91 | "i" = "Found {n_open} occurrence{?s} of {.code /*}.", 92 | "i" = "Found {n_close} occurrence{?s} of {.code */}." 93 | ), 94 | class = "rextendr_error" 95 | ) 96 | } 97 | 98 | # This handles 'nested' comments by calculating nesting depth. 99 | # Whenever `cnt` reaches 0 it indicates that it is an end of a comment block, 100 | # and the next delimiter starts the new block, so we include both, as well as 101 | # the first in the table. 102 | to_replace <- 103 | valid_syms |> 104 | dplyr::mutate( 105 | cnt = cumsum(dplyr::if_else(.data$type == "open", +1L, -1L)) 106 | ) |> 107 | dplyr::filter( 108 | dplyr::lag(.data$cnt) == 0 | .data$cnt == 0 | dplyr::row_number() == 1 109 | ) 110 | 111 | # This handles `*/ text /*` scenarios. 112 | # At this point all 'odd' entries should be 'open', 113 | # all 'even' -- 'close', representing open/close delimiters 114 | # of one comment block. 115 | # If not, comments are malformed. 116 | n_valid <- nrow(to_replace) 117 | if ( 118 | any(to_replace[["type"]][2L * seq_len(n_valid / 2L) - 1L] != "open") || 119 | any(to_replace[["type"]][2L * seq_len(n_valid / 2L)] != "close") 120 | ) { 121 | cli::cli_abort( 122 | c( 123 | "Malformed comments.", 124 | "x" = "{.code /*} and {.code */} are not paired correctly.", 125 | "i" = "This error may be caused by a code fragment like \\ 126 | {.code */ ... /*}." 127 | ), 128 | class = "rextendr_error" 129 | ) 130 | } 131 | # Manual `pivot_wider`. 132 | to_replace <- data.frame( 133 | start_open = dplyr::filter(to_replace, .data$type == "open")[["start"]], 134 | end_close = dplyr::filter(to_replace, .data$type == "close")[["end"]] 135 | ) 136 | 137 | # Replaces each continuous commnet block with whitespaces 138 | # of the same length -- this is needed to preserve line length 139 | # and previously computed positions, and it does not affect 140 | # parsing at later stages. 141 | .open <- to_replace[["start_open"]] 142 | .close <- to_replace[["end_close"]] 143 | gap_size <- (.close - .open) + 1 144 | 145 | result <- stringi::stri_sub_replace_all( 146 | lns, 147 | .open, 148 | .close, 149 | replacement = strrep(fill_with, gap_size) 150 | ) 151 | 152 | result <- stringi::stri_split_lines(result, omit_empty = TRUE)[[1]] 153 | result 154 | } 155 | -------------------------------------------------------------------------------- /R/setup.R: -------------------------------------------------------------------------------- 1 | rextendr_setup <- function(path = ".", cur_version = NULL) { 2 | desc_path <- rprojroot::find_package_root_file("DESCRIPTION", path = path) 3 | if (!file.exists(desc_path)) { 4 | cli::cli_abort( 5 | "{.arg path} ({.path {path}}) does not contain a DESCRIPTION", 6 | class = "rextendr_error" 7 | ) 8 | } 9 | 10 | is_first <- is.na(rextendr_version(desc_path = desc_path)) 11 | 12 | if (is_first) { 13 | cli::cli_alert_info("First time using rextendr. Upgrading automatically...") 14 | } 15 | 16 | update_rextendr_version(desc_path = desc_path, cur_version = cur_version) 17 | update_sys_reqs(desc_path = desc_path) 18 | 19 | invisible(TRUE) 20 | } 21 | 22 | update_rextendr_version <- function(desc_path, cur_version = NULL) { 23 | cur <- cur_version %||% as.character(utils::packageVersion("rextendr")) 24 | prev <- rextendr_version(desc_path = desc_path) 25 | 26 | if (!is.na(cur) && !is.na(prev) && package_version(cur) < package_version(prev)) { 27 | cli::cli_alert_warning(c( 28 | "Installed rextendr is older than the version used with this package", 29 | "You have {.str {cur}} but you need {.str {prev}}" 30 | )) 31 | } else if (!identical(cur, prev)) { 32 | update_description("Config/rextendr/version", cur, desc_path = desc_path) 33 | } 34 | } 35 | 36 | update_sys_reqs <- function(desc_path) { 37 | cur <- "Cargo (Rust's package manager), rustc" 38 | prev <- stringi::stri_trim_both(desc::desc_get("SystemRequirements", file = desc_path)[[1]]) 39 | 40 | if (is.na(prev)) { 41 | update_description("SystemRequirements", cur, desc_path = desc_path) 42 | } else if (!identical(cur, prev)) { 43 | cli::cli_ul( 44 | c( 45 | "The SystemRequirements field in the {.file DESCRIPTION} file is already set.", 46 | "Please update it manually if needed." 47 | ) 48 | ) 49 | } 50 | } 51 | 52 | update_description <- function(field, value, desc_path) { 53 | cli::cli_alert_info("Setting {.var {field}} to {.str {value}} in the {.file DESCRIPTION} file.") 54 | desc::desc_set(field, value, file = desc_path) 55 | } 56 | 57 | rextendr_version <- function(desc_path = ".") { 58 | stringi::stri_trim_both(desc::desc_get("Config/rextendr/version", desc_path)[[1]]) 59 | } 60 | -------------------------------------------------------------------------------- /R/track_rust_source.R: -------------------------------------------------------------------------------- 1 | #' Returns `default` value if `expr` throws an error. 2 | #' 3 | #' This allows to silently handle errors and return a pre-defined 4 | #' default value. 5 | #' @param expr An expression to invoke (can be anything). 6 | #' @param default Value to return if `expr` throws an error. 7 | #' @return Either the result of `expr` or `default`. No 8 | #' type checks or type coersions performed. 9 | #' @example 10 | #' on_error_return_default(stop("This will be consumed"), "You get this instead") 11 | #' @noRd 12 | on_error_return_default <- function(expr, default = NULL) { 13 | tryCatch( 14 | expr, 15 | error = function(e) { 16 | default 17 | } 18 | ) 19 | } 20 | 21 | #' Converts any path to path, relative to the package root 22 | #' E.g., outer_root/some_folder/code/packages/my_package/src/rust/src/lib.rs 23 | #' becomes src/rust/src/lib.rs. 24 | #' Used for pretty printing. 25 | #' Assumes that `path` is within `package_root`. 26 | #' @param path Scalar path to format. 27 | #' @param search_from Path from which package root is looked up. 28 | #' @returns `path`, relative to the package root. 29 | #' @noRd 30 | pretty_rel_single_path <- function(path, search_from = ".") { 31 | stopifnot("`path` may only be one single path" = length(path) == 1) 32 | # Absolute path to the package root. 33 | # If package root cannot be identified, 34 | # an error is thrown, which gets converted into 35 | # `""` using `on_error_return_default`. 36 | package_root <- 37 | on_error_return_default( 38 | normalizePath( 39 | rprojroot::find_package_root_file(path = search_from), 40 | winslash = "/" 41 | ), 42 | default = "" 43 | ) 44 | 45 | # Absolute path. 46 | # `path` may not exist, so `mustWork` suppresses unnecessary warnings. 47 | path <- normalizePath(path, winslash = "/", mustWork = FALSE) 48 | 49 | # If `package_root` is empty or not a parent of `path`, 50 | # return `path` unchanged (for simplicity). 51 | if ( 52 | !nzchar(package_root) || 53 | !stringi::stri_detect_fixed( 54 | str = path, 55 | pattern = package_root, 56 | case_insensitive = TRUE 57 | ) 58 | ) { 59 | return(path) 60 | } 61 | 62 | # If `path` is a subpath of `package_root`, 63 | # then `path` contains `package_root` as a substring. 64 | # This removes `package_root` substring from `path`, 65 | # performing comparison case_insensitively. 66 | path <- stringi::stri_replace_first_fixed( 67 | str = path, 68 | pattern = package_root, 69 | replacement = "", 70 | case_insensitive = TRUE 71 | ) 72 | 73 | # At this point, `path` can potentailly have a leading `/` 74 | # Removes leading `/` if present. 75 | path <- stringi::stri_replace_first_regex(path, "^/", "") 76 | 77 | if (!nzchar(path)) { 78 | path <- "." 79 | } 80 | 81 | path 82 | } 83 | 84 | #' See [pretty_rel_single_path] for implementation details 85 | #' 86 | #' @inheritParams pretty_rel_single_path 87 | #' 88 | #' @noRd 89 | pretty_rel_path <- function(path, search_from = ".") { 90 | map_chr(path, pretty_rel_single_path, search_from = search_from) 91 | } 92 | 93 | get_library_path <- function(path = ".") { 94 | # Constructs path to the library file (e.g., package_name.dll) 95 | file.path( 96 | rprojroot::find_package_root_file("src", path = path), 97 | glue::glue("{pkg_name(path)}{.Platform$dynlib.ext}") 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /R/use_crate.R: -------------------------------------------------------------------------------- 1 | #' Add dependencies to a Cargo.toml manifest file 2 | #' 3 | #' Analogous to `usethis::use_package()` but for crate dependencies. 4 | #' 5 | #' @param crate character scalar, the name of the crate to add 6 | #' @param features character vector, a list of features to include from the 7 | #' crate 8 | #' @param git character scalar, the full URL of the remote Git repository 9 | #' @param version character scalar, the version of the crate to add 10 | #' @param optional boolean scalar, whether to mark the dependency as optional 11 | #' (FALSE by default) 12 | #' @param path character scalar, the package directory 13 | #' @param echo logical scalar, should cargo command and outputs be printed to 14 | #' console (default is TRUE) 15 | #' 16 | #' @details 17 | #' For more details regarding these and other options, see the 18 | #' \href{https://doc.rust-lang.org/cargo/commands/cargo-add.html}{Cargo docs} 19 | #' for `cargo-add`. 20 | #' 21 | #' @return `NULL` (invisibly) 22 | #' 23 | #' @export 24 | #' 25 | #' @examples 26 | #' \dontrun{ 27 | #' # add to [dependencies] 28 | #' use_crate("serde") 29 | #' 30 | #' # add to [dependencies] and [features] 31 | #' use_crate("serde", features = "derive") 32 | #' 33 | #' # add to [dependencies] using github repository as source 34 | #' use_crate("serde", git = "https://github.com/serde-rs/serde") 35 | #' 36 | #' # add to [dependencies] with specific version 37 | #' use_crate("serde", version = "1.0.1") 38 | #' 39 | #' # add to [dependencies] with optional compilation 40 | #' use_crate("serde", optional = TRUE) 41 | #' } 42 | use_crate <- function( 43 | crate, 44 | features = NULL, 45 | git = NULL, 46 | version = NULL, 47 | optional = FALSE, 48 | path = ".", 49 | echo = TRUE) { 50 | check_string(crate, class = "rextendr_error") 51 | check_character(features, allow_null = TRUE, class = "rextendr_error") 52 | check_string(git, allow_null = TRUE, class = "rextendr_error") 53 | check_string(version, allow_null = TRUE, class = "rextendr_error") 54 | check_bool(optional, class = "rextendr_error") 55 | check_string(path, class = "rextendr_error") 56 | check_bool(echo, class = "rextendr_error") 57 | 58 | if (!is.null(version) && !is.null(git)) { 59 | cli::cli_abort( 60 | "Cannot specify a git URL ('{git}') with a version ('{version}').", 61 | class = "rextendr_error" 62 | ) 63 | } 64 | 65 | if (!is.null(version)) { 66 | crate <- paste0(crate, "@", version) 67 | } 68 | 69 | if (!is.null(features)) { 70 | features <- c( 71 | "--features", 72 | paste(crate, features, sep = "/", collapse = ",") 73 | ) 74 | } 75 | 76 | if (!is.null(git)) { 77 | git <- c("--git", git) 78 | } 79 | 80 | if (optional) { 81 | optional <- "--optional" 82 | } else { 83 | optional <- NULL 84 | } 85 | 86 | args <- c( 87 | "add", 88 | crate, 89 | features, 90 | git, 91 | optional, 92 | if (tty_has_colors()) { 93 | "--color=always" 94 | } else { 95 | "--color=never" 96 | } 97 | ) 98 | 99 | run_cargo( 100 | args, 101 | wd = find_extendr_crate(path = path), 102 | echo = echo 103 | ) 104 | 105 | invisible() 106 | } 107 | -------------------------------------------------------------------------------- /R/use_msrv.R: -------------------------------------------------------------------------------- 1 | #' Set the minimum supported rust version (MSRV) 2 | #' 3 | #' `use_msrv()` sets the minimum supported rust version for your R package. 4 | #' 5 | #' @param version character scalar, the minimum supported Rust version. 6 | #' @param path character scalar, path to folder containing DESCRIPTION file. 7 | #' @param overwrite default `FALSE`. Overwrites the `SystemRequirements` field if already set when `TRUE`. 8 | #' @details 9 | #' 10 | #' The minimum supported rust version (MSRV) is determined by the 11 | #' `SystemRequirements` field in a package's `DESCRIPTION` file. For example, to 12 | #' set the MSRV to `1.67.0`, the `SystemRequirements` must have 13 | #' `rustc >= 1.67.0`. 14 | #' 15 | #' By default, there is no MSRV set. However, some crates have features that 16 | #' depend on a minimum version of Rust. As of this writing the version of Rust 17 | #' on CRAN's Fedora machine's is 1.69. If you require a version of Rust that is 18 | #' greater than that, you must set it in your DESCRIPTION file. 19 | #' 20 | #' It is also important to note that if CRAN's machines do not meet the 21 | #' specified MSRV, they will not be able to build a binary of your package. As a 22 | #' consequence, if users try to install the package they will be required to 23 | #' have Rust installed as well. 24 | #' 25 | #' To determine the MSRV of your R package, we recommend installing the 26 | #' `cargo-msrv` cli. You can do so by running `cargo install cargo-msrv`. To 27 | #' determine your MSRV, set your working directory to `src/rust` then run 28 | #' `cargo msrv`. Note that this may take a while. 29 | #' 30 | #' For more details, please see 31 | #' [cargo-msrv](https://github.com/foresterre/cargo-msrv). 32 | #' 33 | #' @return `version` 34 | #' @export 35 | #' 36 | #' @examples 37 | #' \dontrun{ 38 | #' use_msrv("1.67.1") 39 | #' } 40 | #' 41 | use_msrv <- function(version, path = ".", overwrite = FALSE) { 42 | check_string(version, class = "rextendr_error") 43 | check_string(path, class = "rextendr_error") 44 | check_bool(overwrite, class = "rextendr_error") 45 | 46 | msrv_call <- rlang::caller_call() 47 | version <- tryCatch(numeric_version(version), error = function(e) { 48 | cli::cli_abort( 49 | "Invalid version provided", 50 | class = "rextendr_error", 51 | call = msrv_call 52 | ) 53 | }) 54 | 55 | desc_path <- rlang::try_fetch( 56 | rprojroot::find_package_root_file("DESCRIPTION", path = path), 57 | error = function(cnd) { 58 | cli::cli_abort( 59 | "{.arg path} ({.path {path}}) does not contain a DESCRIPTION", 60 | class = "rextendr_error", 61 | call = rlang::env_parent() 62 | ) 63 | } 64 | ) 65 | 66 | cur <- paste("Cargo (Rust's package manager), rustc", paste(">=", version)) 67 | 68 | prev <- desc::desc_get("SystemRequirements", file = desc_path)[[1]] 69 | prev <- stringi::stri_trim_both(prev) 70 | prev_is_default <- identical(prev, "Cargo (Rust's package manager), rustc") 71 | 72 | # if it isn't set update the description or if overwrite is true 73 | if (is.na(prev) || overwrite || prev_is_default) { 74 | update_description("SystemRequirements", cur, desc_path = desc_path) 75 | } else if (!identical(cur, prev) && !overwrite) { 76 | cli::cli_ul( 77 | c( 78 | "The SystemRequirements field in the {.file DESCRIPTION} file is 79 | already set.", 80 | "Please update it manually if needed.", 81 | "{.code SystemRequirements: {cur}}" 82 | ) 83 | ) 84 | } 85 | 86 | invisible(version) 87 | } 88 | -------------------------------------------------------------------------------- /R/use_vscode.R: -------------------------------------------------------------------------------- 1 | #' Set up VS Code configuration for an rextendr project 2 | #' 3 | #' @description This creates a `.vscode` folder (if needed) and populates it with a 4 | #' `settings.json` template. If already exists, it will be updated to include 5 | #' the `rust-analyzer.linkedProjects` setting. 6 | #' 7 | #' @param quiet If `TRUE`, suppress messages. 8 | #' @param overwrite If `TRUE`, overwrite existing files. 9 | #' @details Rust-Analyzer VSCode extension looks for a `Cargo.toml` file in the 10 | #' workspace root by default. This function creates a `.vscode` folder and 11 | #' populates it with a `settings.json` file that sets the workspace root to 12 | #' the `src` directory of the package. This allows you to open the package 13 | #' directory in VSCode and have the Rust-Analyzer extension work correctly. 14 | #' @return `TRUE` (invisibly) if the settings file was created or updated. 15 | #' @export 16 | use_vscode <- function(quiet = FALSE, overwrite = FALSE) { 17 | if (!dir.exists(".vscode")) { 18 | dir.create(".vscode") 19 | } 20 | 21 | usethis::use_build_ignore(file.path(".vscode")) 22 | 23 | settings_path <- file.path(".vscode", "settings.json") 24 | rust_analyzer_path <- "${workspaceFolder}/src/rust/Cargo.toml" 25 | files_associations <- list( 26 | "Makevars.in" = "makefile", 27 | "Makevars.win" = "makefile", 28 | "configure" = "shellscript", 29 | "configure.win" = "shellscript", 30 | "cleanup" = "shellscript", 31 | "cleanup.win" = "shellscript" 32 | ) 33 | 34 | if (file.exists(settings_path) && !overwrite) { 35 | if (!quiet) message("Updating existing .vscode/settings.json") 36 | 37 | # settings.json accepts trailing commas before braces and brackets and {jsonlite} doesn't dig that 38 | tryCatch({ 39 | settings <- jsonlite::read_json(settings_path) 40 | }, error = function(e) { 41 | if (grepl("parse error", e$message)) { 42 | stop( 43 | "Could not parse .vscode/settings.json. Do you have a trailing comma before braces or brackets?\n", 44 | "Original error: : ", e$message 45 | ) 46 | } else { 47 | stop(e$message) 48 | } 49 | }) 50 | 51 | # checking and updating cargo.toml path for Rust-Analyzer 52 | if (!"rust-analyzer.linkedProjects" %in% names(settings)) { 53 | settings[["rust-analyzer.linkedProjects"]] <- list(rust_analyzer_path) 54 | } else if (!rust_analyzer_path %in% settings[["rust-analyzer.linkedProjects"]]) { 55 | settings[["rust-analyzer.linkedProjects"]] <- c( 56 | settings[["rust-analyzer.linkedProjects"]], 57 | rust_analyzer_path 58 | ) 59 | } 60 | 61 | # checking and updating files associations 62 | if (!"files.associations" %in% names(settings)) { 63 | settings[["files.associations"]] <- files_associations 64 | } else { 65 | current_assoc <- settings[["files.associations"]] 66 | for (name in names(files_associations)) { 67 | current_assoc[[name]] <- files_associations[[name]] 68 | } 69 | settings[["files.associations"]] <- current_assoc 70 | } 71 | 72 | jsonlite::write_json( 73 | settings, 74 | settings_path, 75 | auto_unbox = TRUE, 76 | pretty = TRUE 77 | ) 78 | } else { 79 | use_rextendr_template( 80 | "settings.json", 81 | save_as = settings_path, 82 | quiet = quiet, 83 | overwrite = overwrite 84 | ) 85 | } 86 | 87 | invisible(TRUE) 88 | } 89 | 90 | #' @rdname use_vscode 91 | #' @export 92 | use_positron <- use_vscode 93 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | #' Inform the user that a development version of `extendr` is being used. 2 | #' 3 | #' This function returns a string that should be used inside of a `cli` function. 4 | #' See `validate_extendr_features()` for an example. 5 | #' 6 | #' @keywords internal 7 | inf_dev_extendr_used <- function() "Are you using a development version of {.code extendr}?" 8 | 9 | 10 | #' Silence `{cli}` output 11 | #' 12 | #' Use for functions that use cli output that should optionally be suppressed. 13 | #' 14 | #' @examples 15 | #' 16 | #' if (interactive()) { 17 | #' hello_rust <- function(..., quiet = FALSE) { 18 | #' local_quiet_cli(quiet) 19 | #' cli::cli_alert_info("This should be silenced when {.code quiet = TRUE}") 20 | #' } 21 | #' 22 | #' hello_rust() 23 | #' hello_rust(quiet = TRUE) 24 | #' } 25 | #' @keywords internal 26 | local_quiet_cli <- function(quiet, env = rlang::caller_env()) { 27 | if (quiet) { 28 | withr::local_options( 29 | list("cli.default_handler" = function(...) { 30 | }), 31 | .local_envir = env 32 | ) 33 | } 34 | } 35 | 36 | #' Helper function for checking cargo sub-commands. 37 | #' @param args Character vector, arguments to the `cargo` command. Passed to [processx::run()]'s args param. 38 | #' @return Logical scalar indicating if the command was available. 39 | #' @noRd 40 | cargo_command_available <- function(args = "--help") { 41 | !anyNA(try_exec_cmd("cargo", args)) 42 | } 43 | 44 | #' Helper function for executing commands. 45 | #' @param cmd Character scalar, command to execute. 46 | #' @param args Character vector, arguments passed to the command. 47 | #' @return Character vector containing the stdout of the command or `NA_character_` if the command failed. 48 | #' @noRd 49 | try_exec_cmd <- function(cmd, args = character()) { 50 | result <- tryCatch( 51 | processx::run(cmd, args, error_on_status = FALSE), 52 | error = function(...) list(status = -1) 53 | ) 54 | if (result[["status"]] != 0) { 55 | NA_character_ 56 | } else { 57 | stringi::stri_split_lines1(result$stdout) 58 | } 59 | } 60 | 61 | #' Replace missing values in vector 62 | #' 63 | #' @param data vector, data with missing values to replace 64 | #' @param replace scalar, value to substitute for missing values in data 65 | #' @param ... currently ignored 66 | #' 67 | #' @keywords internal 68 | #' @noRd 69 | #' 70 | replace_na <- function(data, replace = NA, ...) { 71 | if (vctrs::vec_any_missing(data)) { 72 | missing <- vctrs::vec_detect_missing(data) 73 | data <- vctrs::vec_assign(data, missing, replace, 74 | x_arg = "data", 75 | value_arg = "replace" 76 | ) 77 | } 78 | data 79 | } 80 | 81 | is_osx <- function() { 82 | sysinf <- Sys.info() 83 | if (!is.null(sysinf)) { 84 | return(identical(sysinf["sysname"], c(sysname = "Darwin"))) 85 | } 86 | grepl("^darwin", R.version$os, ignore.case = TRUE) 87 | } 88 | 89 | is_vscode <- function() { 90 | e <- Sys.getenv(c("VSCODE_PID", "VSCODE_CWD", "VSCODE_IPC_HOOK_CLI", "TERM_PROGRAM")) 91 | if (nzchar(e["VSCODE_PID"]) || nzchar(e["VSCODE_CWD"]) || nzchar(e["VSCODE_IPC_HOOK_CLI"]) || tolower(e["TERM_PROGRAM"]) == "vscode") { # nolint 92 | return(TRUE) 93 | } 94 | FALSE 95 | } 96 | 97 | is_positron <- function() { 98 | e <- Sys.getenv(c("POSITRON", "POSITRON_LONG_VERSION", "POSITRON_MODE", "POSITRON_VERSION")) 99 | if (nzchar(e["POSITRON"]) || nzchar(e["POSITRON_LONG_VERSION"]) || nzchar(e["POSITRON_MODE"]) || nzchar(e["POSITRON_VERSION"])) { # nolint 100 | return(TRUE) 101 | } 102 | FALSE 103 | } 104 | -------------------------------------------------------------------------------- /R/write_file.R: -------------------------------------------------------------------------------- 1 | #' Writes text to file 2 | #' 3 | #' This function is a wrapper around [`brio::write_lines()`]. 4 | #' It also supports verbose output, similar to [`usethis::write_over()`], 5 | #' controlled by the `quiet` parameter. 6 | #' @param text Character vector containing text to write. 7 | #' @param path A string giving the file path to write to. 8 | #' @param search_root_from This parameter only affects messages displayed to the user. 9 | #' It has no effect on where the file is written. 10 | #' It gets passed to [`pretty_rel_path()`] if `quiet = FALSE`. 11 | #' It is unused otherwise. 12 | #' @param quiet Logical scalar indicating whether the output should be quiet (`TRUE`) 13 | #' or verbose (`FALSE`). 14 | #' @param overwrite Logical scalar indicating whether the file in the `path` should be overwritten. 15 | #' If `FALSE` and the file already exists, the function will do nothing. 16 | #' @return The output of [`brio::write_lines()`] (invisibly). 17 | #' @noRd 18 | write_file <- function(text, path, search_root_from = ".", quiet = FALSE, overwrite = TRUE) { 19 | if (isFALSE(overwrite) && file.exists(path)) { 20 | cli::cli_alert("File {.path {save_as}} already exists. Skip writing the file.") 21 | return(invisible(NULL)) 22 | } 23 | 24 | output <- brio::write_lines(text = text, path = path) 25 | if (!isTRUE(quiet)) { 26 | rel_path <- pretty_rel_path(path, search_from = search_root_from) # nolint: object_usage_linter 27 | cli::cli_alert_success("Writing {.path {rel_path}}") 28 | } 29 | invisible(output) 30 | } 31 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | .onLoad <- function(...) { 2 | # register the extendr knitr chunk engine if knitr is available 3 | if (requireNamespace("knitr", quietly = TRUE)) { 4 | knitr::knit_engines$set( 5 | extendr = eng_extendr, 6 | extendrsrc = eng_extendrsrc 7 | ) 8 | } 9 | 10 | # Setting default options 11 | # If rextendr options are already set, do not override 12 | # NULL values are present for reference and may later be replaced 13 | # by concrete values 14 | 15 | rextendr_opts <- list( 16 | # Controls default Rust toolchain; NULL corresponds to system's default 17 | rextendr.toolchain = NULL, 18 | # rextendr.toolchain = "nightly", # use 'nightly' tool chain 19 | 20 | # Overrides Rust dependencies; mainly used for development 21 | rextendr.patch.crates_io = NULL, # most recent extendr crates on crates.io 22 | # rextendr.patch.crates_io = list( # most recent extendr crates on github 23 | # `extendr-api` = list(git = "https://github.com/extendr/extendr") 24 | # ), 25 | 26 | # Version of 'extendr_api' to be used 27 | rextendr.extendr_deps = list( 28 | `extendr-api` = "*" 29 | ), 30 | rextendr.extendr_dev_deps = list( 31 | `extendr-api` = list(git = "https://github.com/extendr/extendr") 32 | ) 33 | ) 34 | 35 | 36 | id_opts_to_set <- !(names(rextendr_opts) %in% names(options())) 37 | 38 | options(rextendr_opts[id_opts_to_set]) 39 | } 40 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://extendr.github.io/rextendr/ 2 | template: 3 | bootstrap: 5 4 | development: 5 | mode: auto 6 | 7 | destination: docs 8 | 9 | reference: 10 | - title: Compiling and running Rust code 11 | contents: 12 | - rust_source 13 | - rust_function 14 | - rust_eval 15 | - eng_extendr 16 | 17 | - title: Package development 18 | contents: 19 | - use_extendr 20 | - use_crate 21 | - document 22 | - register_extendr 23 | - write_license_note 24 | - clean 25 | - cran 26 | - vendor_pkgs 27 | - use_msrv 28 | - use_vscode 29 | 30 | - title: Various utility functions 31 | contents: 32 | - to_toml 33 | - make_module_macro 34 | - rust_sitrep 35 | - read_cargo_metadata 36 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "header, reach, files" 3 | require_changes: true 4 | behavior: "new" # Only post on new commits 5 | coverage: 6 | status: 7 | project: 8 | default: 9 | target: 70% 10 | threshold: 5% 11 | patch: 12 | default: 13 | target: 70% 14 | threshold: 5% 15 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## revdepcheck results 2 | 3 | We checked 1 reverse dependencies, comparing R CMD check results across CRAN and dev versions of this package. 4 | 5 | * We saw 0 new problems 6 | * We failed to check 0 packages 7 | 8 | * Note from developers: The package `tergo` was subjected to revdep check, however the check was inconclusive. The process was manually interrupted after 30 minutes without producing a definitive result. According to [CRAN](https://cran.r-project.org/web/packages/tergo/index.html), `tergo` has `rextendr` (this package) pinned to version `0.3.1`, so releasing new version of `rextendr` should not cause any regressions in `tergo`. -------------------------------------------------------------------------------- /inst/rstudio/templates/project/extendr.dcf: -------------------------------------------------------------------------------- 1 | Binding: create_extendr_package 2 | Title: R package with extendr 3 | Subtitle: Create an R package with Rust extensions. 4 | Caption: Create an R package with Rust extensions. 5 | 6 | Parameter: roxygen 7 | Widget: CheckboxInput 8 | Label: Use roxygen2 9 | Default: On 10 | Position: left 11 | 12 | Parameter: check_name 13 | Widget: CheckboxInput 14 | Label: Validate package name 15 | Default: On 16 | Position: left 17 | 18 | Parameter: crate_name 19 | Widget: TextInput 20 | Label: Rust crate name 21 | Position: right 22 | 23 | Parameter: lib_name 24 | Widget: TextInput 25 | Label: Rust library name 26 | Position: right 27 | 28 | Parameter: edition 29 | Widget: SelectInput 30 | Label: Rust edition 31 | Fields: 2021, 2018 32 | Default: 2021 33 | Position: right 34 | -------------------------------------------------------------------------------- /inst/templates/Cargo.toml: -------------------------------------------------------------------------------- 1 | {{{cargo_toml_content}}} 2 | -------------------------------------------------------------------------------- /inst/templates/Makevars.in: -------------------------------------------------------------------------------- 1 | TARGET_DIR = ./rust/target 2 | LIBDIR = $(TARGET_DIR)/@LIBDIR@ 3 | STATLIB = $(LIBDIR)/lib{{{lib_name}}}.a 4 | PKG_LIBS = -L$(LIBDIR) -l{{{lib_name}}} 5 | 6 | all: $(SHLIB) rust_clean 7 | 8 | .PHONY: $(STATLIB) 9 | 10 | $(SHLIB): $(STATLIB) 11 | 12 | CARGOTMP = $(CURDIR)/.cargo 13 | VENDOR_DIR = $(CURDIR)/vendor 14 | 15 | 16 | # RUSTFLAGS appends --print=native-static-libs to ensure that 17 | # the correct linkers are used. Use this for debugging if need. 18 | # 19 | # CRAN note: Cargo and Rustc versions are reported during 20 | # configure via tools/msrv.R. 21 | # 22 | # vendor.tar.xz, if present, is unzipped and used for offline compilation. 23 | $(STATLIB): 24 | 25 | if [ -f ./rust/vendor.tar.xz ]; then \ 26 | tar xf rust/vendor.tar.xz && \ 27 | mkdir -p $(CARGOTMP) && \ 28 | cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ 29 | fi 30 | 31 | export CARGO_HOME=$(CARGOTMP) && \ 32 | export PATH="$(PATH):$(HOME)/.cargo/bin" && \ 33 | RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --lib @PROFILE@ --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) @TARGET@ 34 | 35 | # Always clean up CARGOTMP 36 | rm -Rf $(CARGOTMP); 37 | 38 | rust_clean: $(SHLIB) 39 | rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ 40 | 41 | clean: 42 | rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) 43 | -------------------------------------------------------------------------------- /inst/templates/Makevars.win.in: -------------------------------------------------------------------------------- 1 | TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu 2 | 3 | TARGET_DIR = ./rust/target 4 | LIBDIR = $(TARGET_DIR)/$(TARGET)/@LIBDIR@ 5 | STATLIB = $(LIBDIR)/lib{{{lib_name}}}.a 6 | PKG_LIBS = -L$(LIBDIR) -l{{{lib_name}}} -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll 7 | 8 | all: $(SHLIB) rust_clean 9 | 10 | .PHONY: $(STATLIB) 11 | 12 | $(SHLIB): $(STATLIB) 13 | 14 | CARGOTMP = $(CURDIR)/.cargo 15 | VENDOR_DIR = vendor 16 | 17 | $(STATLIB): 18 | mkdir -p $(TARGET_DIR)/libgcc_mock 19 | touch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a 20 | 21 | if [ -f ./rust/vendor.tar.xz ]; then \ 22 | tar xf rust/vendor.tar.xz && \ 23 | mkdir -p $(CARGOTMP) && \ 24 | cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ 25 | fi 26 | 27 | # Build the project using Cargo with additional flags 28 | export CARGO_HOME=$(CARGOTMP) && \ 29 | export LIBRARY_PATH="$(LIBRARY_PATH);$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ 30 | RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --target=$(TARGET) --lib @PROFILE@ --manifest-path=rust/Cargo.toml --target-dir=$(TARGET_DIR) 31 | 32 | # Always clean up CARGOTMP 33 | rm -Rf $(CARGOTMP); 34 | 35 | rust_clean: $(SHLIB) 36 | rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ 37 | 38 | clean: 39 | rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) 40 | -------------------------------------------------------------------------------- /inst/templates/_gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | target 5 | .cargo 6 | -------------------------------------------------------------------------------- /inst/templates/config.R: -------------------------------------------------------------------------------- 1 | # Note: Any variables prefixed with `.` are used for text 2 | # replacement in the Makevars.in and Makevars.win.in 3 | 4 | # check the packages MSRV first 5 | source("tools/msrv.R") 6 | 7 | # check DEBUG and NOT_CRAN environment variables 8 | env_debug <- Sys.getenv("DEBUG") 9 | env_not_cran <- Sys.getenv("NOT_CRAN") 10 | 11 | # check if the vendored zip file exists 12 | vendor_exists <- file.exists("src/rust/vendor.tar.xz") 13 | 14 | is_not_cran <- env_not_cran != "" 15 | is_debug <- env_debug != "" 16 | 17 | if (is_debug) { 18 | # if we have DEBUG then we set not cran to true 19 | # CRAN is always release build 20 | is_not_cran <- TRUE 21 | message("Creating DEBUG build.") 22 | } 23 | 24 | if (!is_not_cran) { 25 | message("Building for CRAN.") 26 | } 27 | 28 | # we set cran flags only if NOT_CRAN is empty and if 29 | # the vendored crates are present. 30 | .cran_flags <- ifelse( 31 | !is_not_cran && vendor_exists, 32 | "-j 2 --offline", 33 | "" 34 | ) 35 | 36 | # when DEBUG env var is present we use `--debug` build 37 | .profile <- ifelse(is_debug, "", "--release") 38 | .clean_targets <- ifelse(is_debug, "", "$(TARGET_DIR)") 39 | 40 | # We specify this target when building for webR 41 | webr_target <- "wasm32-unknown-emscripten" 42 | 43 | # here we check if the platform we are building for is webr 44 | is_wasm <- identical(R.version$platform, webr_target) 45 | 46 | # print to terminal to inform we are building for webr 47 | if (is_wasm) { 48 | message("Building for WebR") 49 | } 50 | 51 | # we check if we are making a debug build or not 52 | # if so, the LIBDIR environment variable becomes: 53 | # LIBDIR = $(TARGET_DIR)/{wasm32-unknown-emscripten}/debug 54 | # this will be used to fill out the LIBDIR env var for Makevars.in 55 | target_libpath <- if (is_wasm) "wasm32-unknown-emscripten" else NULL 56 | cfg <- if (is_debug) "debug" else "release" 57 | 58 | # used to replace @LIBDIR@ 59 | .libdir <- paste(c(target_libpath, cfg), collapse = "/") 60 | 61 | # use this to replace @TARGET@ 62 | # we specify the target _only_ on webR 63 | # there may be use cases later where this can be adapted or expanded 64 | .target <- ifelse(is_wasm, paste0("--target=", webr_target), "") 65 | 66 | # read in the Makevars.in file checking 67 | is_windows <- .Platform[["OS.type"]] == "windows" 68 | 69 | # if windows we replace in the Makevars.win.in 70 | mv_fp <- ifelse( 71 | is_windows, 72 | "src/Makevars.win.in", 73 | "src/Makevars.in" 74 | ) 75 | 76 | # set the output file 77 | mv_ofp <- ifelse( 78 | is_windows, 79 | "src/Makevars.win", 80 | "src/Makevars" 81 | ) 82 | 83 | # delete the existing Makevars{.win} 84 | if (file.exists(mv_ofp)) { 85 | message("Cleaning previous `", mv_ofp, "`.") 86 | invisible(file.remove(mv_ofp)) 87 | } 88 | 89 | # read as a single string 90 | mv_txt <- readLines(mv_fp) 91 | 92 | # replace placeholder values 93 | new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> 94 | gsub("@PROFILE@", .profile, x = _) |> 95 | gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> 96 | gsub("@LIBDIR@", .libdir, x = _) |> 97 | gsub("@TARGET@", .target, x = _) 98 | 99 | message("Writing `", mv_ofp, "`.") 100 | con <- file(mv_ofp, open = "wb") 101 | writeLines(new_txt, con, sep = "\n") 102 | close(con) 103 | 104 | message("`tools/config.R` has finished.") 105 | -------------------------------------------------------------------------------- /inst/templates/configure: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | : "${R_HOME=`R RHOME`}" 3 | "${R_HOME}/bin/Rscript" tools/config.R 4 | -------------------------------------------------------------------------------- /inst/templates/configure.win: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/config.R -------------------------------------------------------------------------------- /inst/templates/entrypoint.c: -------------------------------------------------------------------------------- 1 | // We need to forward routine registration from C to Rust 2 | // to avoid the linker removing the static library. 3 | 4 | void R_init_{{{mod_name}}}_extendr(void *dll); 5 | 6 | void R_init_{{{mod_name}}}(void *dll) { 7 | R_init_{{{mod_name}}}_extendr(dll); 8 | } 9 | -------------------------------------------------------------------------------- /inst/templates/extendr-wrappers.R: -------------------------------------------------------------------------------- 1 | # nolint start 2 | 3 | #' @docType package 4 | #' @usage NULL 5 | #' @useDynLib {{{pkg_name}}}, .registration = TRUE 6 | NULL 7 | 8 | #' Return string `"Hello world!"` to R. 9 | #' @export 10 | hello_world <- function() .Call(wrap__hello_world) 11 | 12 | # nolint end 13 | -------------------------------------------------------------------------------- /inst/templates/lib.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::prelude::*; 2 | 3 | /// Return string `"Hello world!"` to R. 4 | /// @export 5 | #[extendr] 6 | fn hello_world() -> &'static str { 7 | "Hello world!" 8 | } 9 | 10 | // Macro to generate exports. 11 | // This ensures exported functions are registered with R. 12 | // See corresponding C code in `entrypoint.c`. 13 | extendr_module! { 14 | mod {{{mod_name}}}; 15 | fn hello_world; 16 | } 17 | -------------------------------------------------------------------------------- /inst/templates/msrv.R: -------------------------------------------------------------------------------- 1 | # read the DESCRIPTION file 2 | desc <- read.dcf("DESCRIPTION") 3 | 4 | if (!"SystemRequirements" %in% colnames(desc)) { 5 | fmt <- c( 6 | "`SystemRequirements` not found in `DESCRIPTION`.", 7 | "Please specify `SystemRequirements: Cargo (Rust's package manager), rustc`" 8 | ) 9 | stop(paste(fmt, collapse = "\n")) 10 | } 11 | 12 | # extract system requirements 13 | sysreqs <- desc[, "SystemRequirements"] 14 | 15 | # check that cargo and rustc is found 16 | if (!grepl("cargo", sysreqs, ignore.case = TRUE)) { 17 | stop("You must specify `Cargo (Rust's package manager)` in your `SystemRequirements`") 18 | } 19 | 20 | if (!grepl("rustc", sysreqs, ignore.case = TRUE)) { 21 | stop("You must specify `Cargo (Rust's package manager), rustc` in your `SystemRequirements`") 22 | } 23 | 24 | # split into parts 25 | parts <- strsplit(sysreqs, ", ")[[1]] 26 | 27 | # identify which is the rustc 28 | rustc_ver <- parts[grepl("rustc", parts)] 29 | 30 | # perform checks for the presence of rustc and cargo on the OS 31 | no_cargo_msg <- c( 32 | "----------------------- [CARGO NOT FOUND]--------------------------", 33 | "The 'cargo' command was not found on the PATH. Please install Cargo", 34 | "from: https://www.rust-lang.org/tools/install", 35 | "", 36 | "Alternatively, you may install Cargo from your OS package manager:", 37 | " - Debian/Ubuntu: apt-get install cargo", 38 | " - Fedora/CentOS: dnf install cargo", 39 | " - macOS: brew install rust", 40 | "-------------------------------------------------------------------" 41 | ) 42 | 43 | no_rustc_msg <- c( 44 | "----------------------- [RUST NOT FOUND]---------------------------", 45 | "The 'rustc' compiler was not found on the PATH. Please install", 46 | paste(rustc_ver, "or higher from:"), 47 | "https://www.rust-lang.org/tools/install", 48 | "", 49 | "Alternatively, you may install Rust from your OS package manager:", 50 | " - Debian/Ubuntu: apt-get install rustc", 51 | " - Fedora/CentOS: dnf install rustc", 52 | " - macOS: brew install rust", 53 | "-------------------------------------------------------------------" 54 | ) 55 | 56 | # Add {user}/.cargo/bin to path before checking 57 | new_path <- paste0( 58 | Sys.getenv("PATH"), 59 | ":", 60 | paste0(Sys.getenv("HOME"), "/.cargo/bin") 61 | ) 62 | 63 | # set the path with the new path 64 | Sys.setenv("PATH" = new_path) 65 | 66 | # check for rustc installation 67 | rustc_version <- tryCatch( 68 | system("rustc --version", intern = TRUE), 69 | error = function(e) { 70 | stop(paste(no_rustc_msg, collapse = "\n")) 71 | } 72 | ) 73 | 74 | # check for cargo installation 75 | cargo_version <- tryCatch( 76 | system("cargo --version", intern = TRUE), 77 | error = function(e) { 78 | stop(paste(no_cargo_msg, collapse = "\n")) 79 | } 80 | ) 81 | 82 | # helper function to extract versions 83 | extract_semver <- function(ver) { 84 | if (grepl("\\d+\\.\\d+(\\.\\d+)?", ver)) { 85 | sub(".*?(\\d+\\.\\d+(\\.\\d+)?).*", "\\1", ver) 86 | } else { 87 | NA 88 | } 89 | } 90 | 91 | # get the MSRV 92 | msrv <- extract_semver(rustc_ver) 93 | 94 | # extract current version 95 | current_rust_version <- extract_semver(rustc_version) 96 | 97 | # perform check 98 | if (!is.na(msrv)) { 99 | # -1 when current version is later 100 | # 0 when they are the same 101 | # 1 when MSRV is newer than current 102 | is_msrv <- utils::compareVersion(msrv, current_rust_version) 103 | if (is_msrv == 1) { 104 | fmt <- paste0( 105 | "\n------------------ [UNSUPPORTED RUST VERSION]------------------\n", 106 | "- Minimum supported Rust version is %s.\n", 107 | "- Installed Rust version is %s.\n", 108 | "---------------------------------------------------------------" 109 | ) 110 | stop(sprintf(fmt, msrv, current_rust_version)) 111 | } 112 | } 113 | 114 | # print the versions 115 | versions_fmt <- "Using %s\nUsing %s" 116 | message(sprintf(versions_fmt, cargo_version, rustc_version)) 117 | -------------------------------------------------------------------------------- /inst/templates/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "${workspaceFolder}/src/rust/Cargo.toml" 4 | ], 5 | "files.associations": { 6 | "Makevars.in": "makefile", 7 | "Makevars.win": "makefile", 8 | "configure": "shellscript", 9 | "configure.win": "shellscript", 10 | "cleanup": "shellscript", 11 | "cleanup.win": "shellscript" 12 | } 13 | } -------------------------------------------------------------------------------- /inst/templates/win.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | R_init_{{{mod_name}}} 3 | -------------------------------------------------------------------------------- /man/clean.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/clean.R 3 | \name{clean} 4 | \alias{clean} 5 | \title{Clean Rust binaries and package cache.} 6 | \usage{ 7 | clean(path = ".", echo = TRUE) 8 | } 9 | \arguments{ 10 | \item{path}{character scalar, path to R package root.} 11 | 12 | \item{echo}{logical scalar, should cargo command and outputs be printed to 13 | console (default is \code{TRUE})} 14 | } 15 | \value{ 16 | character vector with names of all deleted files (invisibly). 17 | } 18 | \description{ 19 | Removes Rust binaries (such as \code{.dll}/\code{.so} libraries), C wrapper object files, 20 | invokes \verb{cargo clean} to reset cargo target directory 21 | (found by default at \verb{pkg_root/src/rust/target/}). 22 | Useful when Rust code should be recompiled from scratch. 23 | } 24 | \examples{ 25 | \dontrun{ 26 | clean() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /man/cran.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/cran-compliance.R 3 | \name{cran} 4 | \alias{cran} 5 | \title{CRAN compliant extendr packages} 6 | \description{ 7 | R packages developed using extendr are not immediately ready to 8 | be published to CRAN. The extendr package template ensures that 9 | CRAN publication is (farily) painless. 10 | } 11 | \section{CRAN requirements}{ 12 | 13 | 14 | In order to publish a Rust based package on CRAN it must meet certain 15 | requirements. These are: 16 | \itemize{ 17 | \item Rust dependencies are vendored 18 | \item The package is compiled offline 19 | \item the \code{DESCRIPTION} file's \code{SystemRequirements} field contains \verb{Cargo (Rust's package manager), rustc} 20 | } 21 | 22 | The extendr templates handle all of this \emph{except} vendoring dependencies. 23 | This must be done prior to publication using \code{\link[=vendor_pkgs]{vendor_pkgs()}}. 24 | 25 | In addition, it is important to make sure that CRAN maintainers 26 | are aware that the package they are checking contains Rust code. 27 | Depending on which and how many crates are used as a dependencies 28 | the \code{vendor.tar.xz} will be larger than a few megabytes. If a 29 | built package is larger than 5mbs CRAN may reject the submission. 30 | 31 | To prevent rejection make a note in your \code{cran-comments.md} file 32 | (create one using \code{\link[usethis:use_cran_comments]{usethis::use_cran_comments()}}) along the lines of 33 | "The package tarball is 6mb because Rust dependencies are vendored within src/rust/vendor.tar.xz which is 5.9mb." 34 | } 35 | 36 | -------------------------------------------------------------------------------- /man/document.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rextendr_document.R 3 | \name{document} 4 | \alias{document} 5 | \title{Compile Rust code and generate package documentation.} 6 | \usage{ 7 | document(pkg = ".", quiet = FALSE, roclets = NULL) 8 | } 9 | \arguments{ 10 | \item{pkg}{The package to use, can be a file path to the package or a 11 | package object. See \code{\link[devtools:as.package]{as.package()}} for more information.} 12 | 13 | \item{quiet}{if \code{TRUE} suppresses output from this function.} 14 | 15 | \item{roclets}{Character vector of roclet names to use with package. 16 | The default, \code{NULL}, uses the roxygen \code{roclets} option, 17 | which defaults to \code{c("collate", "namespace", "rd")}.} 18 | } 19 | \value{ 20 | No return value, called for side effects. 21 | } 22 | \description{ 23 | The function \code{rextendr::document()} updates the package documentation for an 24 | R package that uses \code{extendr} code, taking into account any changes that were 25 | made in the Rust code. It is a wrapper for \code{\link[devtools:document]{devtools::document()}}, and it 26 | executes \code{extendr}-specific routines before calling \code{\link[devtools:document]{devtools::document()}}. 27 | Specifically, it ensures that Rust code is recompiled (when necessary) and that 28 | up-to-date R wrappers are generated before regenerating the package documentation. 29 | } 30 | -------------------------------------------------------------------------------- /man/eng_extendr.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/knitr_engine.R 3 | \name{eng_extendr} 4 | \alias{eng_extendr} 5 | \alias{eng_extendrsrc} 6 | \title{Knitr engines} 7 | \usage{ 8 | eng_extendr(options) 9 | 10 | eng_extendrsrc(options) 11 | } 12 | \arguments{ 13 | \item{options}{A list of chunk options.} 14 | } 15 | \value{ 16 | A character string representing the engine output. 17 | } 18 | \description{ 19 | Two knitr engines that enable code chunks of type \code{extendr} (individual Rust 20 | statements to be evaluated via \code{\link[=rust_eval]{rust_eval()}}) and \code{extendrsrc} (Rust functions 21 | or classes that will be exported to R via \code{\link[=rust_source]{rust_source()}}). 22 | } 23 | -------------------------------------------------------------------------------- /man/figures/rextendr-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extendr/rextendr/5d3ae4ad8a2d8160cdd6b58f934d58bf75ae9b12/man/figures/rextendr-logo.png -------------------------------------------------------------------------------- /man/inf_dev_extendr_used.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{inf_dev_extendr_used} 4 | \alias{inf_dev_extendr_used} 5 | \title{Inform the user that a development version of \code{extendr} is being used.} 6 | \usage{ 7 | inf_dev_extendr_used() 8 | } 9 | \description{ 10 | This function returns a string that should be used inside of a \code{cli} function. 11 | See \code{validate_extendr_features()} for an example. 12 | } 13 | \keyword{internal} 14 | -------------------------------------------------------------------------------- /man/local_quiet_cli.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{local_quiet_cli} 4 | \alias{local_quiet_cli} 5 | \title{Silence \code{{cli}} output} 6 | \usage{ 7 | local_quiet_cli(quiet, env = rlang::caller_env()) 8 | } 9 | \description{ 10 | Use for functions that use cli output that should optionally be suppressed. 11 | } 12 | \examples{ 13 | 14 | if (interactive()) { 15 | hello_rust <- function(..., quiet = FALSE) { 16 | local_quiet_cli(quiet) 17 | cli::cli_alert_info("This should be silenced when {.code quiet = TRUE}") 18 | } 19 | 20 | hello_rust() 21 | hello_rust(quiet = TRUE) 22 | } 23 | } 24 | \keyword{internal} 25 | -------------------------------------------------------------------------------- /man/make_module_macro.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/make_module_macro.R 3 | \name{make_module_macro} 4 | \alias{make_module_macro} 5 | \title{Generate extendr module macro for Rust source} 6 | \usage{ 7 | make_module_macro(code, module_name = "rextendr") 8 | } 9 | \arguments{ 10 | \item{code}{Character vector containing Rust code.} 11 | 12 | \item{module_name}{Module name} 13 | } 14 | \value{ 15 | Character vector holding the contents of the generated macro statement. 16 | } 17 | \description{ 18 | Read some Rust source code, find functions or implementations with the 19 | \verb{#[extendr]} attribute, and generate an \verb{extendr_module!} macro statement. 20 | } 21 | \details{ 22 | This function uses simple regular expressions to do the Rust parsing and 23 | can get confused by valid Rust code. It is only meant as a convenience for 24 | simple use cases. In particular, it cannot currently handle implementations 25 | for generics. 26 | } 27 | \keyword{internal} 28 | -------------------------------------------------------------------------------- /man/read_cargo_metadata.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/read_cargo_metadata.R 3 | \name{read_cargo_metadata} 4 | \alias{read_cargo_metadata} 5 | \title{Retrieve metadata for packages and workspaces} 6 | \usage{ 7 | read_cargo_metadata(path = ".", dependencies = FALSE, echo = FALSE) 8 | } 9 | \arguments{ 10 | \item{path}{character scalar, the R package directory} 11 | 12 | \item{dependencies}{Default \code{FALSE}. A logical scalar, whether to include 13 | all recursive dependencies in stdout.} 14 | 15 | \item{echo}{Default \code{FALSE}. A logical scalar, should cargo command and 16 | outputs be printed to the console.} 17 | } 18 | \value{ 19 | A \code{list} including the following elements: 20 | \itemize{ 21 | \item \code{packages} 22 | \item \code{workspace_members} 23 | \item \code{workspace_default_members} 24 | \item \code{resolve} 25 | \item \code{target_directory} 26 | \item \code{version} 27 | \item \code{workspace_root} 28 | \item \code{metadata} 29 | } 30 | } 31 | \description{ 32 | Retrieve metadata for packages and workspaces 33 | } 34 | \details{ 35 | For more details, see 36 | \href{https://doc.rust-lang.org/cargo/commands/cargo-metadata.html}{Cargo docs} 37 | for \code{cargo-metadata}. See especially "JSON Format" to get a sense of what you 38 | can expect to find in the returned list. 39 | } 40 | \examples{ 41 | \dontrun{ 42 | read_cargo_metadata() 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /man/register_extendr.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/register_extendr.R 3 | \name{register_extendr} 4 | \alias{register_extendr} 5 | \title{Register the extendr module of a package with R} 6 | \usage{ 7 | register_extendr(path = ".", quiet = FALSE, force = FALSE, compile = NA) 8 | } 9 | \arguments{ 10 | \item{path}{Path from which package root is looked up.} 11 | 12 | \item{quiet}{Logical indicating whether any progress messages should be 13 | generated or not.} 14 | 15 | \item{force}{Logical indicating whether to force regenerating 16 | \code{R/extendr-wrappers.R} even when it doesn't seem to need updated. (By 17 | default, generation is skipped when it's newer than the DLL).} 18 | 19 | \item{compile}{Logical indicating whether to recompile DLLs: 20 | \describe{ 21 | \item{\code{TRUE}}{always recompiles} 22 | \item{\code{NA}}{recompiles if needed (i.e., any source files or manifest file are newer than the DLL)} 23 | \item{\code{FALSE}}{never recompiles} 24 | }} 25 | } 26 | \value{ 27 | (Invisibly) Path to the file containing generated wrappers. 28 | } 29 | \description{ 30 | This function generates wrapper code corresponding to the extendr module 31 | for an R package. This is useful in package development, where we generally 32 | want appropriate R code wrapping the Rust functions implemented via extendr. 33 | In most development settings, you will not want to call this function directly, 34 | but instead call \code{rextendr::document()}. 35 | } 36 | \details{ 37 | The function \code{register_extendr()} compiles the package Rust code if 38 | required, and then the wrapper code is retrieved from the compiled 39 | Rust code and saved into \code{R/extendr-wrappers.R}. Afterwards, you will have 40 | to re-document and then re-install the package for the wrapper functions to 41 | take effect. 42 | } 43 | \seealso{ 44 | \code{\link[=document]{document()}} 45 | } 46 | -------------------------------------------------------------------------------- /man/rextendr.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rextendr.R 3 | \docType{package} 4 | \name{rextendr} 5 | \alias{rextendr-package} 6 | \alias{rextendr} 7 | \title{Call Rust code from R using the 'extendr' Crate} 8 | \description{ 9 | The rextendr package implements functions to interface with Rust code from R. 10 | See \code{\link[=rust_source]{rust_source()}} for details. 11 | } 12 | \seealso{ 13 | Useful links: 14 | \itemize{ 15 | \item \url{https://extendr.github.io/rextendr/} 16 | \item \url{https://github.com/extendr/rextendr} 17 | \item Report bugs at \url{https://github.com/extendr/rextendr/issues} 18 | } 19 | 20 | } 21 | \author{ 22 | \strong{Maintainer}: Ilia Kosenkov \email{ilia.kosenkov@outlook.com} (\href{https://orcid.org/0000-0001-5563-7840}{ORCID}) 23 | 24 | Authors: 25 | \itemize{ 26 | \item Claus O. Wilke \email{wilke@austin.utexas.edu} (\href{https://orcid.org/0000-0002-7470-9261}{ORCID}) 27 | \item Andy Thomason \email{andy@andythomason.com} 28 | \item Mossa M. Reimert \email{mossa@sund.ku.dk} 29 | \item Malcolm Barrett \email{malcolmbarrett@gmail.com} (\href{https://orcid.org/0000-0003-0299-5825}{ORCID}) 30 | } 31 | 32 | Other contributors: 33 | \itemize{ 34 | \item Josiah Parry \email{josiah.parry@gmail.con} (\href{https://orcid.org/0000-0001-9910-865X}{ORCID}) [contributor] 35 | \item Kenneth Vernon \email{kenneth.b.vernon@gmail.com} (\href{https://orcid.org/0000-0003-0098-5092}{ORCID}) [contributor] 36 | \item Alberson Miranda \email{albersonmiranda@hotmail.com} (\href{https://orcid.org/0000-0001-9252-4175}{ORCID}) [contributor] 37 | } 38 | 39 | } 40 | \keyword{internal} 41 | -------------------------------------------------------------------------------- /man/rust_eval.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/eval.R 3 | \name{rust_eval} 4 | \alias{rust_eval} 5 | \title{Evaluate Rust code} 6 | \usage{ 7 | rust_eval(code, env = parent.frame(), ...) 8 | } 9 | \arguments{ 10 | \item{code}{Input rust code.} 11 | 12 | \item{env}{The R environment in which the Rust code will be evaluated.} 13 | 14 | \item{...}{Other parameters handed off to \code{\link[=rust_function]{rust_function()}}.} 15 | } 16 | \value{ 17 | The return value generated by the Rust code. 18 | } 19 | \description{ 20 | Compile and evaluate one or more Rust expressions. If the last 21 | expression in the Rust code returns a value (i.e., does not end with 22 | \verb{;}), then this value is returned to R. The value returned does not need 23 | to be of type \code{Robj}, as long as it can be cast into this type with 24 | \code{.into()}. This conversion is done automatically, so you don't have to 25 | worry about it in your code. 26 | } 27 | \examples{ 28 | \dontrun{ 29 | # Rust code without return value, called only for its side effects 30 | rust_eval( 31 | code = 'rprintln!("hello from Rust!");' 32 | ) 33 | 34 | # Rust code with return value 35 | rust_eval( 36 | code = " 37 | let x = 5; 38 | let y = 7; 39 | let z = x * y; 40 | z // return to R; rust_eval() takes care of type conversion code 41 | " 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /man/rust_sitrep.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rust_sitrep.R 3 | \name{rust_sitrep} 4 | \alias{rust_sitrep} 5 | \title{Report on Rust infrastructure} 6 | \usage{ 7 | rust_sitrep() 8 | } 9 | \value{ 10 | Nothing 11 | } 12 | \description{ 13 | Prints out a detailed report on the state of Rust infrastructure on the host machine. 14 | } 15 | -------------------------------------------------------------------------------- /man/to_toml.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/toml_serialization.R 3 | \name{to_toml} 4 | \alias{to_toml} 5 | \title{Convert R \code{list()} into toml-compatible format.} 6 | \usage{ 7 | to_toml(..., .str_as_literal = TRUE, .format_int = "\%d", .format_dbl = "\%g") 8 | } 9 | \arguments{ 10 | \item{...}{A list from which toml is constructed. 11 | Supports nesting and tidy evaluation.} 12 | 13 | \item{.str_as_literal}{Logical indicating whether to treat 14 | strings as literal (single quotes no escapes) or 15 | basic (escaping some sequences) ones. Default is \code{TRUE}.} 16 | 17 | \item{.format_int, .format_dbl}{Character scalar describing 18 | number formatting. Compatible with \code{sprintf}.} 19 | } 20 | \value{ 21 | A character vector, each element corresponds to 22 | one line of the resulting output. 23 | } 24 | \description{ 25 | \code{\link[=to_toml]{to_toml()}} can be used to build \code{Cargo.toml}. 26 | The cargo manifest can be represented in terms of 27 | R objects, allowing limited validation and syntax verification. 28 | This function converts manifests written using R objects into 29 | toml representation, applying basic formatting, 30 | which is ideal for generating cargo 31 | manifests at runtime. 32 | } 33 | \examples{ 34 | # Produces [workspace] with no children 35 | to_toml(workspace = NULL) 36 | 37 | to_toml(patch.crates_io = list(`extendr-api` = list(git = "git-ref"))) 38 | 39 | # Single-element arrays are distinguished from scalars 40 | # using explicitly set `dim` 41 | to_toml(lib = list(`crate-type` = array("cdylib", 1))) 42 | } 43 | -------------------------------------------------------------------------------- /man/use_crate.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/use_crate.R 3 | \name{use_crate} 4 | \alias{use_crate} 5 | \title{Add dependencies to a Cargo.toml manifest file} 6 | \usage{ 7 | use_crate( 8 | crate, 9 | features = NULL, 10 | git = NULL, 11 | version = NULL, 12 | optional = FALSE, 13 | path = ".", 14 | echo = TRUE 15 | ) 16 | } 17 | \arguments{ 18 | \item{crate}{character scalar, the name of the crate to add} 19 | 20 | \item{features}{character vector, a list of features to include from the 21 | crate} 22 | 23 | \item{git}{character scalar, the full URL of the remote Git repository} 24 | 25 | \item{version}{character scalar, the version of the crate to add} 26 | 27 | \item{optional}{boolean scalar, whether to mark the dependency as optional 28 | (FALSE by default)} 29 | 30 | \item{path}{character scalar, the package directory} 31 | 32 | \item{echo}{logical scalar, should cargo command and outputs be printed to 33 | console (default is TRUE)} 34 | } 35 | \value{ 36 | \code{NULL} (invisibly) 37 | } 38 | \description{ 39 | Analogous to \code{usethis::use_package()} but for crate dependencies. 40 | } 41 | \details{ 42 | For more details regarding these and other options, see the 43 | \href{https://doc.rust-lang.org/cargo/commands/cargo-add.html}{Cargo docs} 44 | for \code{cargo-add}. 45 | } 46 | \examples{ 47 | \dontrun{ 48 | # add to [dependencies] 49 | use_crate("serde") 50 | 51 | # add to [dependencies] and [features] 52 | use_crate("serde", features = "derive") 53 | 54 | # add to [dependencies] using github repository as source 55 | use_crate("serde", git = "https://github.com/serde-rs/serde") 56 | 57 | # add to [dependencies] with specific version 58 | use_crate("serde", version = "1.0.1") 59 | 60 | # add to [dependencies] with optional compilation 61 | use_crate("serde", optional = TRUE) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /man/use_extendr.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/use_extendr.R 3 | \name{use_extendr} 4 | \alias{use_extendr} 5 | \title{Set up a package for use with Rust extendr code} 6 | \usage{ 7 | use_extendr( 8 | path = ".", 9 | crate_name = NULL, 10 | lib_name = NULL, 11 | quiet = FALSE, 12 | overwrite = NULL, 13 | edition = c("2021", "2018") 14 | ) 15 | } 16 | \arguments{ 17 | \item{path}{File path to the package for which to generate wrapper code.} 18 | 19 | \item{crate_name}{String that is used as the name of the Rust crate. 20 | If \code{NULL}, sanitized R package name is used instead.} 21 | 22 | \item{lib_name}{String that is used as the name of the Rust library. 23 | If \code{NULL}, sanitized R package name is used instead.} 24 | 25 | \item{quiet}{Logical indicating whether any progress messages should be 26 | generated or not.} 27 | 28 | \item{overwrite}{Logical scalar or \code{NULL} indicating whether the files in the \code{path} should be overwritten. 29 | If \code{NULL} (default), the function will ask the user whether each file should 30 | be overwritten in an interactive session or do nothing in a non-interactive session. 31 | If \code{FALSE} and each file already exists, the function will do nothing. 32 | If \code{TRUE}, all files will be overwritten.} 33 | 34 | \item{edition}{String indicating which Rust edition is used; Default \code{"2021"}.} 35 | } 36 | \value{ 37 | A logical value (invisible) indicating whether any package files were 38 | generated or not. 39 | } 40 | \description{ 41 | Create the scaffolding needed to add Rust extendr code to an R package. \code{use_extendr()} 42 | adds a small Rust library with a single Rust function that returns the string 43 | \code{"Hello world!"}. It also adds wrapper code so this Rust function can be called from 44 | R with \code{hello_world()}. 45 | } 46 | -------------------------------------------------------------------------------- /man/use_msrv.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/use_msrv.R 3 | \name{use_msrv} 4 | \alias{use_msrv} 5 | \title{Set the minimum supported rust version (MSRV)} 6 | \usage{ 7 | use_msrv(version, path = ".", overwrite = FALSE) 8 | } 9 | \arguments{ 10 | \item{version}{character scalar, the minimum supported Rust version.} 11 | 12 | \item{path}{character scalar, path to folder containing DESCRIPTION file.} 13 | 14 | \item{overwrite}{default \code{FALSE}. Overwrites the \code{SystemRequirements} field if already set when \code{TRUE}.} 15 | } 16 | \value{ 17 | \code{version} 18 | } 19 | \description{ 20 | \code{use_msrv()} sets the minimum supported rust version for your R package. 21 | } 22 | \details{ 23 | The minimum supported rust version (MSRV) is determined by the 24 | \code{SystemRequirements} field in a package's \code{DESCRIPTION} file. For example, to 25 | set the MSRV to \verb{1.67.0}, the \code{SystemRequirements} must have 26 | \verb{rustc >= 1.67.0}. 27 | 28 | By default, there is no MSRV set. However, some crates have features that 29 | depend on a minimum version of Rust. As of this writing the version of Rust 30 | on CRAN's Fedora machine's is 1.69. If you require a version of Rust that is 31 | greater than that, you must set it in your DESCRIPTION file. 32 | 33 | It is also important to note that if CRAN's machines do not meet the 34 | specified MSRV, they will not be able to build a binary of your package. As a 35 | consequence, if users try to install the package they will be required to 36 | have Rust installed as well. 37 | 38 | To determine the MSRV of your R package, we recommend installing the 39 | \code{cargo-msrv} cli. You can do so by running \verb{cargo install cargo-msrv}. To 40 | determine your MSRV, set your working directory to \code{src/rust} then run 41 | \verb{cargo msrv}. Note that this may take a while. 42 | 43 | For more details, please see 44 | \href{https://github.com/foresterre/cargo-msrv}{cargo-msrv}. 45 | } 46 | \examples{ 47 | \dontrun{ 48 | use_msrv("1.67.1") 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /man/use_vscode.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/use_vscode.R 3 | \name{use_vscode} 4 | \alias{use_vscode} 5 | \alias{use_positron} 6 | \title{Set up VS Code configuration for an rextendr project} 7 | \usage{ 8 | use_vscode(quiet = FALSE, overwrite = FALSE) 9 | 10 | use_positron(quiet = FALSE, overwrite = FALSE) 11 | } 12 | \arguments{ 13 | \item{quiet}{If \code{TRUE}, suppress messages.} 14 | 15 | \item{overwrite}{If \code{TRUE}, overwrite existing files.} 16 | } 17 | \value{ 18 | \code{TRUE} (invisibly) if the settings file was created or updated. 19 | } 20 | \description{ 21 | This creates a \code{.vscode} folder (if needed) and populates it with a 22 | \code{settings.json} template. If already exists, it will be updated to include 23 | the \code{rust-analyzer.linkedProjects} setting. 24 | } 25 | \details{ 26 | Rust-Analyzer VSCode extension looks for a \code{Cargo.toml} file in the 27 | workspace root by default. This function creates a \code{.vscode} folder and 28 | populates it with a \code{settings.json} file that sets the workspace root to 29 | the \code{src} directory of the package. This allows you to open the package 30 | directory in VSCode and have the Rust-Analyzer extension work correctly. 31 | } 32 | -------------------------------------------------------------------------------- /man/vendor_pkgs.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/cran-compliance.R 3 | \name{vendor_pkgs} 4 | \alias{vendor_pkgs} 5 | \title{Vendor Rust dependencies} 6 | \usage{ 7 | vendor_pkgs(path = ".", quiet = FALSE, overwrite = NULL) 8 | } 9 | \arguments{ 10 | \item{path}{File path to the package for which to generate wrapper code.} 11 | 12 | \item{quiet}{Logical indicating whether any progress messages should be 13 | generated or not.} 14 | 15 | \item{overwrite}{Logical scalar or \code{NULL} indicating whether the files in the \code{path} should be overwritten. 16 | If \code{NULL} (default), the function will ask the user whether each file should 17 | be overwritten in an interactive session or do nothing in a non-interactive session. 18 | If \code{FALSE} and each file already exists, the function will do nothing. 19 | If \code{TRUE}, all files will be overwritten.} 20 | } 21 | \value{ 22 | \itemize{ 23 | \item \code{vendor_pkgs()} returns a data.frame with two columns \code{crate} and \code{version} 24 | } 25 | } 26 | \description{ 27 | \code{vendor_pkgs()} is used to package the dependencies as required by CRAN. 28 | It executes \verb{cargo vendor} on your behalf creating a \verb{vendor/} directory and a 29 | compressed \code{vendor.tar.xz} which will be shipped with package itself. 30 | If you have modified your dependencies, you will need need to repackage 31 | } 32 | \examples{ 33 | \dontrun{ 34 | vendor_pkgs() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /man/write_license_note.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/license_note.R 3 | \name{write_license_note} 4 | \alias{write_license_note} 5 | \title{Generate LICENSE.note file.} 6 | \usage{ 7 | write_license_note(path = ".", quiet = FALSE, force = TRUE) 8 | } 9 | \arguments{ 10 | \item{path}{character scalar, the R package directory} 11 | 12 | \item{quiet}{logical scalar, whether to signal successful writing of 13 | LICENSE.note (default is \code{FALSE})} 14 | 15 | \item{force}{logical scalar, whether to regenerate LICENSE.note if 16 | LICENSE.note already exists (default is \code{TRUE})} 17 | } 18 | \value{ 19 | text printed to LICENSE.note (invisibly). 20 | } 21 | \description{ 22 | LICENSE.note generated by this function contains information about all 23 | recursive dependencies in Rust crate. 24 | } 25 | \examples{ 26 | \dontrun{ 27 | write_license_note() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /principles.md: -------------------------------------------------------------------------------- 1 | # rextendr design prinicples 2 | 3 | This guide documents important internal coding conventions used in rextendr. 4 | 5 | ## Communicating with the user 6 | 7 | rextendr uses the cli package to format messages to the user, which are generally then routed through `rlang::inform()`. Most UI will happen through `ui_*()` functions: 8 | 9 | | function | purpose | 10 | |----------|---------------------------------------------------------------------------------------------------------------| 11 | | `cli::cli_alert_success()` | communicate that rextendr has done something successfully, such as wrote a file | 12 | | `cli::cli_alert_info()` | provide extra information to the user | 13 | | `cli::cli_alert_warning()` | warn the user about something (note: this is still condition of class `message`, not `warning`) | 14 | | `cli::cli_ul()` | indicate that the user has something to do | 15 | | `cli::cli_alert_danger()` | tell the user that something has gone wrong (note: this still is a condition of class `message`, not `error`) | 16 | 17 | 18 | 19 | [`{cli}`](https://cli.r-lib.org/) supports [glue-based interpolation](https://cli.r-lib.org/articles/semantic-cli.html#interpolation) and [inline text formatting](https://cli.r-lib.org/articles/semantic-cli.html#inline-text-formatting). 20 | 21 | ## Throwing and testing errors 22 | 23 | Pass all errors via `cli::cli_abort()`. Ensure that the class `rextendr_error` is specified. You can also add details by providing a named vector. See `?cli::cli_abort()` and `?cli::cli_bulelts()` for the `message` argument. 24 | 25 | ```r 26 | cli::cli_abort( 27 | c( 28 | "Unable to register the extendr module.", 29 | "x" = "Could not find file {.file src/entrypoint.c}.", 30 | "*" = "Are you sure this package is using extendr Rust code?" 31 | ), 32 | class = "rextendr_error" 33 | ) 34 | ``` 35 | 36 | ## Silencing messages 37 | 38 | `{cli}` is used to verbosely inform the user. The user should be able to optionally silence functions that have particularly verbose output by setting `quiet` argument to `TRUE`—see `?rust_source` for example. 39 | 40 | Silencing `cli` output should be done with the helper `local_quiet_cli(quiet)`. When `quiet = TRUE` all, `cli` output is suppressed. 41 | -------------------------------------------------------------------------------- /rextendr.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: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /tests/data/inner_1/rust_source.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::prelude::*; 2 | 3 | #[extendr] 4 | pub fn test_method_1() -> i32 { 1i32 } 5 | 6 | extendr_module! { 7 | mod test_module; 8 | fn test_method_1; 9 | } -------------------------------------------------------------------------------- /tests/data/inner_2/rust_source.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::prelude::*; 2 | 3 | #[extendr] 4 | pub fn test_method_2() -> i32 { 2i32 } 5 | 6 | extendr_module! { 7 | mod test_module; 8 | fn test_method_2; 9 | } -------------------------------------------------------------------------------- /tests/data/ndarray_example.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::prelude::*; 2 | use ndarray::ArrayView2; 3 | 4 | #[extendr] 5 | fn matrix_sum(input: ArrayView2) -> Rfloat { 6 | input.iter().sum() 7 | } 8 | 9 | extendr_module! { 10 | mod rextendr; 11 | fn matrix_sum; 12 | } 13 | -------------------------------------------------------------------------------- /tests/data/rust_source.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::prelude::*; 2 | 3 | #[extendr] 4 | pub fn test_method() -> i32 { 42i32 } 5 | 6 | extendr_module! { 7 | mod test_module; 8 | fn test_method; 9 | } -------------------------------------------------------------------------------- /tests/data/test-knitr-engine-source-01.Rmd: -------------------------------------------------------------------------------- 1 | ```{r, include = FALSE} 2 | knitr::opts_chunk$set( 3 | message = FALSE, 4 | collapse = TRUE, 5 | comment = "#>" 6 | ) 7 | ``` 8 | 9 | 10 | Basic use example: 11 | 12 | ```{r} 13 | library(rextendr) 14 | 15 | # create a Rust function 16 | rust_function("fn add(a:f64, b:f64) -> f64 { a + b }") 17 | 18 | # call it from R 19 | add(2.5, 4.7) 20 | ``` 21 | 22 | The package also enables a new chunk type for knitr, `extendr`, which compiles and evaluates Rust code. For example, a code chunk such as this one: 23 | ````markdown 24 | `r ''````{extendr} 25 | rprintln!("Hello from Rust!"); 26 | 27 | let x = 5; 28 | let y = 7; 29 | let z = x*y; 30 | 31 | z 32 | ``` 33 | ```` 34 | 35 | would create the following output in the knitted document: 36 | ```{extendr} 37 | rprintln!("Hello from Rust!"); 38 | 39 | let x = 5; 40 | let y = 7; 41 | let z = x*y; 42 | 43 | z 44 | ``` 45 | 46 | Define variable `_x`: 47 | 48 | ```{extendr chunk_x, eval = FALSE} 49 | let _x = 1; 50 | ``` 51 | 52 | Define variable `_y`: 53 | 54 | ```{extendr chunk_y, eval = FALSE} 55 | let _y = 2; 56 | ``` 57 | 58 | Print: 59 | 60 | ```{extendr out, preamble = c("chunk_x", "chunk_y")} 61 | rprintln!("x = {}, y = {}", _x, _y); 62 | ``` 63 | 64 | ```{extendrsrc engine.opts = list(dependencies = list(`pulldown-cmark` = "0.8"))} 65 | use pulldown_cmark::{Parser, Options, html}; 66 | 67 | #[extendr] 68 | fn md_to_html(input: &str) -> String { 69 | let mut options = Options::empty(); 70 | options.insert(Options::ENABLE_TABLES); 71 | let parser = Parser::new_ext(input, options); 72 | let mut output = String::new(); 73 | html::push_html(&mut output, parser); 74 | output 75 | } 76 | ``` 77 | 78 | ```{r} 79 | md_text <- "# The story of the fox 80 | The quick brown fox **jumps over** the lazy dog. 81 | The quick *brown fox* jumps over the lazy dog." 82 | 83 | md_to_html(md_text) 84 | ``` 85 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview 7 | # * https://testthat.r-lib.org/articles/special-files.html 8 | 9 | library(testthat) 10 | library(rextendr) 11 | 12 | test_check("rextendr") 13 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/cran-compliance.md: -------------------------------------------------------------------------------- 1 | # vendor_pkgs() vendors dependencies 2 | 3 | Code 4 | cat_file("src", "rust", "vendor-config.toml") 5 | Output 6 | [source.crates-io] 7 | replace-with = "vendored-sources" 8 | 9 | [source.vendored-sources] 10 | directory = "vendor" 11 | 12 | --- 13 | 14 | Code 15 | package_versions 16 | Output 17 | crate version 18 | 1 build-print *.*.* 19 | 2 extendr-api *.*.* 20 | 3 extendr-ffi *.*.* 21 | 4 extendr-macros *.*.* 22 | 5 once_cell *.*.* 23 | 6 paste *.*.* 24 | 7 proc-macro2 *.*.* 25 | 8 quote *.*.* 26 | 9 syn *.*.* 27 | 10 unicode-ident *.*.* 28 | 29 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/knitr-engine.md: -------------------------------------------------------------------------------- 1 | # Snapshot test of knitr-engine 2 | 3 | Code 4 | cat_file(output) 5 | Output 6 | 7 | 8 | 9 | Basic use example: 10 | 11 | 12 | ``` r 13 | library(rextendr) 14 | 15 | # create a Rust function 16 | rust_function("fn add(a:f64, b:f64) -> f64 { a + b }") 17 | 18 | # call it from R 19 | add(2.5, 4.7) 20 | #> [1] 7.2 21 | ``` 22 | 23 | The package also enables a new chunk type for knitr, `extendr`, which compiles and evaluates Rust code. For example, a code chunk such as this one: 24 | ````markdown 25 | ```{extendr} 26 | rprintln!("Hello from Rust!"); 27 | 28 | let x = 5; 29 | let y = 7; 30 | let z = x*y; 31 | 32 | z 33 | ``` 34 | ```` 35 | 36 | would create the following output in the knitted document: 37 | 38 | ``` rust 39 | rprintln!("Hello from Rust!"); 40 | 41 | let x = 5; 42 | let y = 7; 43 | let z = x*y; 44 | 45 | z 46 | #> Hello from Rust! 47 | #> [1] 35 48 | ``` 49 | 50 | Define variable `_x`: 51 | 52 | 53 | ``` rust 54 | let _x = 1; 55 | ``` 56 | 57 | Define variable `_y`: 58 | 59 | 60 | ``` rust 61 | let _y = 2; 62 | ``` 63 | 64 | Print: 65 | 66 | 67 | ``` rust 68 | rprintln!("x = {}, y = {}", _x, _y); 69 | #> x = 1, y = 2 70 | ``` 71 | 72 | 73 | ``` rust 74 | use pulldown_cmark::{Parser, Options, html}; 75 | 76 | #[extendr] 77 | fn md_to_html(input: &str) -> String { 78 | let mut options = Options::empty(); 79 | options.insert(Options::ENABLE_TABLES); 80 | let parser = Parser::new_ext(input, options); 81 | let mut output = String::new(); 82 | html::push_html(&mut output, parser); 83 | output 84 | } 85 | ``` 86 | 87 | 88 | ``` r 89 | md_text <- "# The story of the fox 90 | The quick brown fox **jumps over** the lazy dog. 91 | The quick *brown fox* jumps over the lazy dog." 92 | 93 | md_to_html(md_text) 94 | #> [1] "

The story of the fox

\n

The quick brown fox jumps over the lazy dog.\nThe quick brown fox jumps over the lazy dog.

\n" 95 | ``` 96 | 97 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/license_note.md: -------------------------------------------------------------------------------- 1 | # LICENSE.note is generated properly 2 | 3 | Code 4 | cat_file("LICENSE.note") 5 | Output 6 | The binary compiled from the source code of this package contains the following Rust crates: 7 | 8 | 9 | ------------------------------------------------------------- 10 | 11 | Name: build-print 12 | Repository: NA 13 | Authors: sam0x17 14 | License: MIT 15 | 16 | ------------------------------------------------------------- 17 | 18 | Name: extendr-api 19 | Repository: https://github.com/extendr/extendr 20 | Authors: andy-thomason, Thomas Down, Mossa Merhi Reimert, Josiah Parry, Claus O. Wilke, Hiroaki Yutani, Ilia A. Kosenkov, Michael Milton 21 | License: MIT 22 | 23 | ------------------------------------------------------------- 24 | 25 | Name: extendr-ffi 26 | Repository: https://github.com/extendr/extendr 27 | Authors: andy-thomason, Thomas Down, Mossa Merhi Reimert, Josiah Parry, Claus O. Wilke, Hiroaki Yutani, Ilia A. Kosenkov, Michael Milton 28 | License: MIT 29 | 30 | ------------------------------------------------------------- 31 | 32 | Name: extendr-macros 33 | Repository: https://github.com/extendr/extendr 34 | Authors: andy-thomason, Thomas Down, Mossa Merhi Reimert, Josiah Parry, Claus O. Wilke, Hiroaki Yutani, Ilia A. Kosenkov, Michael Milton 35 | License: MIT 36 | 37 | ------------------------------------------------------------- 38 | 39 | Name: once_cell 40 | Repository: https://github.com/matklad/once_cell 41 | Authors: Aleksey Kladov 42 | License: MIT OR Apache-2.0 43 | 44 | ------------------------------------------------------------- 45 | 46 | Name: paste 47 | Repository: https://github.com/dtolnay/paste 48 | Authors: David Tolnay 49 | License: MIT OR Apache-2.0 50 | 51 | ------------------------------------------------------------- 52 | 53 | Name: proc-macro2 54 | Repository: https://github.com/dtolnay/proc-macro2 55 | Authors: David Tolnay, Alex Crichton 56 | License: MIT OR Apache-2.0 57 | 58 | ------------------------------------------------------------- 59 | 60 | Name: quote 61 | Repository: https://github.com/dtolnay/quote 62 | Authors: David Tolnay 63 | License: MIT OR Apache-2.0 64 | 65 | ------------------------------------------------------------- 66 | 67 | Name: syn 68 | Repository: https://github.com/dtolnay/syn 69 | Authors: David Tolnay 70 | License: MIT OR Apache-2.0 71 | 72 | ------------------------------------------------------------- 73 | 74 | Name: unicode-ident 75 | Repository: https://github.com/dtolnay/unicode-ident 76 | Authors: David Tolnay 77 | License: (MIT OR Apache-2.0) AND Unicode-3.0 78 | 79 | ------------------------------------------------------------- 80 | 81 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/rust-sitrep.md: -------------------------------------------------------------------------------- 1 | # `cargo` or `rustup` are not found 2 | 3 | Code 4 | rust_sitrep() 5 | Message 6 | Rust infrastructure sitrep: 7 | x "rustup": not found 8 | x "cargo": not found 9 | x Cannot determine host, toolchain, and targets without "rustup" 10 | x "cargo" is required to build rextendr-powered packages 11 | i It is recommended to use "rustup" to manage "cargo" and "rustc" 12 | i See for installation instructions 13 | 14 | # `cargo` is found, `rustup` is missing 15 | 16 | Code 17 | rust_sitrep() 18 | Message 19 | Rust infrastructure sitrep: 20 | x "rustup": not found 21 | v "cargo": 1.0.0 (0000000 0000-00-00) 22 | x Cannot determine host, toolchain, and targets without "rustup" 23 | i It is recommended to use "rustup" to manage "cargo" and "rustc" 24 | i See for installation instructions 25 | 26 | # `rustup` is found, `cargo` is missing 27 | 28 | Code 29 | rust_sitrep() 30 | Message 31 | Rust infrastructure sitrep: 32 | v "rustup": 1.0.0 (0000000 0000-00-00) 33 | x "cargo": not found 34 | i host: arch-pc-os-tool 35 | i toolchain: stable-arch-pc-os-tool (default) 36 | i target: arch-pc-os-tool 37 | x "cargo" is required to build rextendr-powered packages 38 | i It is recommended to use "rustup" to manage "cargo" and "rustc" 39 | i See for installation instructions 40 | 41 | # `cargo` and`rustup` are found 42 | 43 | Code 44 | rust_sitrep() 45 | Message 46 | Rust infrastructure sitrep: 47 | v "rustup": 1.0.0 (0000000 0000-00-00) 48 | v "cargo": 1.0.0 (0000000 0000-00-00) 49 | i host: arch-pc-os-tool 50 | i toolchain: stable-arch-pc-os-tool (default) 51 | i target: arch-pc-os-tool 52 | 53 | # No toolchains found 54 | 55 | Code 56 | rust_sitrep() 57 | Message 58 | Rust infrastructure sitrep: 59 | v "rustup": 1.0.0 (0000000 0000-00-00) 60 | v "cargo": 1.0.0 (0000000 0000-00-00) 61 | i host: arch-pc-os-tool 62 | ! Toolchain stable-arch-pc-os-tool is required to be installed and set as default 63 | i Run `rustup toolchain install stable-arch-pc-os-tool` to install it 64 | i Run `rustup default stable-arch-pc-os-tool` to make it default 65 | 66 | # Wrong toolchain found 67 | 68 | Code 69 | rust_sitrep() 70 | Message 71 | Rust infrastructure sitrep: 72 | v "rustup": 1.0.0 (0000000 0000-00-00) 73 | v "cargo": 1.0.0 (0000000 0000-00-00) 74 | i host: arch-pc-os-tool 75 | i toolchain: not-a-valid-toolchain 76 | ! Toolchain stable-arch-pc-os-tool is required to be installed and set as default 77 | i Run `rustup toolchain install stable-arch-pc-os-tool` to install it 78 | i Run `rustup default stable-arch-pc-os-tool` to make it default 79 | 80 | # Wrong toolchain is set as default 81 | 82 | Code 83 | rust_sitrep() 84 | Message 85 | Rust infrastructure sitrep: 86 | v "rustup": 1.0.0 (0000000 0000-00-00) 87 | v "cargo": 1.0.0 (0000000 0000-00-00) 88 | i host: arch-pc-os-tool 89 | i toolchains: not-a-valid-toolchain (default) and stable-arch-pc-os-tool 90 | ! This toolchain should be default: stable-arch-pc-os-tool 91 | i Run e.g. `rustup default stable-arch-pc-os-tool` 92 | 93 | # Required target is not available 94 | 95 | Code 96 | rust_sitrep() 97 | Message 98 | Rust infrastructure sitrep: 99 | v "rustup": 1.0.0 (0000000 0000-00-00) 100 | v "cargo": 1.0.0 (0000000 0000-00-00) 101 | i host: arch-pc-os-tool 102 | i toolchains: not-a-valid-toolchain and stable-arch-pc-os-tool (default) 103 | i targets: wrong-target-1 and wrong-target-2 104 | ! Target required-target is required on this host machine 105 | i Run `rustup target add required-target` to install it 106 | 107 | # Detects host when default toolchain is not set 108 | 109 | Code 110 | rust_sitrep() 111 | Message 112 | Rust infrastructure sitrep: 113 | v "rustup": 1.0.0 (0000000 0000-00-00) 114 | x "cargo": not found 115 | i host: arch-pc-os-tool 116 | i toolchain: stable-arch-pc-os-tool 117 | ! This toolchain should be default: stable-arch-pc-os-tool 118 | i Run e.g. `rustup default stable-arch-pc-os-tool` 119 | x "cargo" is required to build rextendr-powered packages 120 | i It is recommended to use "rustup" to manage "cargo" and "rustc" 121 | i See for installation instructions 122 | 123 | -------------------------------------------------------------------------------- /tests/testthat/helper-toolchain.R: -------------------------------------------------------------------------------- 1 | # Toolchain is a string, so can be read as is 2 | toolchain <- Sys.getenv("REXTENDR_TOOLCHAIN") 3 | if (!is.null(toolchain) && nzchar(toolchain)) { 4 | options(rextendr.toolchain = toolchain) 5 | message( 6 | paste0( 7 | ">> {rextendr}: Using toolchain from 'REXTENDR_TOOLCHAIN': ", 8 | toolchain 9 | ) 10 | ) 11 | } 12 | 13 | # Patch is represented as vector of character. 14 | # In environment variable different crates are separated using ';' 15 | # E.g., "extendr-api = { path = "/local/path" };extendr-macros = 16 | # { git = \"https://github.com/extendr/extendr\" }" 17 | patch <- Sys.getenv("REXTENDR_PATCH_CRATES_IO") 18 | if (!is.null(patch) && nzchar(patch)) { 19 | patch_val <- gsub( 20 | "([a-zA-Z0-9_\\-\\.]+)(?=\\s*=)", "`\\1`", 21 | patch, 22 | perl = TRUE 23 | ) 24 | patch_val <- gsub("\\{", "list(", patch_val) 25 | patch_val <- gsub("\\}", ")", patch_val) 26 | patch_val <- gsub(";", ", ", patch_val) 27 | patch_expr <- parse(text = paste0("list(", patch_val, ")")) 28 | 29 | options(rextendr.patch.crates_io = eval(patch_expr)) 30 | message( 31 | paste0( 32 | ">> {rextendr}: Using cargo patch from 'REXTENDR_PATCH_CRATES_IO': ", 33 | patch 34 | ) 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | #' Does code throw an rextendr_error? 2 | #' 3 | #' `expect_rextendr_error()` expects an error of class `rextendr_error`, as 4 | #' thrown by `ui_throw()`. 5 | #' 6 | #' @param ... arguments passed to [testthat::expect_error()] 7 | expect_rextendr_error <- function(...) { 8 | testthat::expect_error(..., class = "rextendr_error") 9 | } 10 | 11 | #' Create a local package 12 | #' 13 | #' `local_package()` creates a self-cleaning test package via usethis and withr. 14 | #' It also sets the local working directory and usethis project to the temporary 15 | #' package. These settings are reverted and the package removed via 16 | #' `withr::defer()`. This clean-up happens at the end of the local scope, 17 | #' usually the end of a `test_that()` call. 18 | #' 19 | #' @param nm The name of the temporary package 20 | #' @param envir An environment where `withr::defer()`'s exit handler is 21 | #' attached, usually the `parent.frame()` to exist locally 22 | #' 23 | #' @return A path to the root package directory 24 | local_package <- function(nm, envir = parent.frame()) { 25 | local_temp_dir(envir = envir) 26 | dir <- usethis::create_package(nm) 27 | setwd(dir) 28 | local_proj_set(envir = envir) 29 | 30 | invisible(dir) 31 | } 32 | 33 | #' Create a local temporary directory 34 | #' 35 | #' `local_temp_dir()` creates a local temporary directory and sets the created 36 | #' directory as the working directory. These are then cleaned up with 37 | #' `withr::defer()` at the end of the scope, usually the end of the `test_that()` 38 | #' scope. 39 | #' 40 | #' @param envir An environment where `withr::defer()`'s exit handler is 41 | #' attached, usually the `parent.frame()` to exist locally 42 | #' 43 | #' @return A path to the temporary directory 44 | local_temp_dir <- function(..., envir = parent.frame()) { 45 | current_wd <- getwd() 46 | path <- file.path(tempfile(), ...) 47 | dir.create(path, recursive = TRUE) 48 | 49 | setwd(path) 50 | 51 | withr::defer( 52 | { 53 | setwd(current_wd) 54 | usethis::proj_set(NULL) 55 | unlink(path) 56 | }, 57 | envir = envir 58 | ) 59 | 60 | invisible(path) 61 | } 62 | 63 | #' Set a local usethis project 64 | #' 65 | #' `local_proj_set()` locally sets a new usethis project. The project is 66 | #' reverted with `withr::defer()` at the end of the scope, usually the end of 67 | #' the `test_that()` scope. 68 | #' 69 | #' @param envir An environment where `withr::defer()`'s exit handler is 70 | #' attached, usually the `parent.frame()` to exist locally 71 | local_proj_set <- function(envir = parent.frame()) { 72 | old_proj <- usethis::proj_set(getwd(), force = TRUE) 73 | withr::defer(usethis::proj_set(old_proj), envir = envir) 74 | } 75 | 76 | #' Helper function for snapshot testing. 77 | #' Wraps `brio::read_file` and writes content to output using `cat`. 78 | #' @param ... Path to the file being read. 79 | #' @noRd 80 | cat_file <- function(...) { 81 | cat(brio::read_file(file.path(...))) 82 | } 83 | 84 | #' Helper function for skipping tests when cargo subcommand is unavailable 85 | #' @param args Character vector, arguments to the `cargo` command. Pass to [processx::run()]'s args param. 86 | skip_if_cargo_unavailable <- function(args = "--help") { 87 | tryCatch( 88 | { 89 | processx::run("cargo", args, error_on_status = TRUE) 90 | }, 91 | error = function(e) { 92 | message <- paste0("`cargo ", paste0(args, collapse = " "), "` is not available.") 93 | testthat::skip(message) 94 | } 95 | ) 96 | } 97 | 98 | #' Helper function for skipping tests when the test possibly fails because of 99 | #' the path length limit. This only happens on R (<= 4.2) on Windows. 100 | skip_on_R42_win <- function() { 101 | if (.Platform$OS.type == "windows" && getRversion() < "4.3") { 102 | testthat::skip("Long path is not supported by this version of Rtools.") 103 | } 104 | } 105 | 106 | skip_if_opted_out_of_dev_tests <- function() { 107 | env_var <- Sys.getenv("REXTENDR_SKIP_DEV_TESTS") |> 108 | stringi::stri_trim_both() |> 109 | stringi::stri_trans_tolower() 110 | 111 | if (env_var == "true" || env_var == "1") { 112 | testthat::skip("Dev extendr tests disabled") 113 | } 114 | } 115 | 116 | #' Mask any version in snapshot files 117 | #' @param snapshot_lines Character vector, lines of the snapshot file 118 | #' @example 119 | #' expect_snapshot(some_operation(), transform = mask_any_version) 120 | #' @noRd 121 | mask_any_version <- function(snapshot_lines) { 122 | stringi::stri_replace_all_regex( 123 | snapshot_lines, 124 | "\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?\\s*", 125 | "*.*.*" 126 | ) 127 | } 128 | -------------------------------------------------------------------------------- /tests/testthat/setup.R: -------------------------------------------------------------------------------- 1 | withr::local_options(usethis.quiet = TRUE, .local_envir = teardown_env()) 2 | -------------------------------------------------------------------------------- /tests/testthat/test-clean.R: -------------------------------------------------------------------------------- 1 | test_that("rextendr::clean() removes cargo target directory & binaries", { 2 | skip_if_not_installed("usethis") 3 | skip_if_not_installed("devtools") 4 | skip_on_cran() 5 | skip_if_cargo_unavailable() 6 | skip_on_R42_win() 7 | 8 | path <- local_package("testpkg") 9 | use_extendr() 10 | document() 11 | 12 | expect_equal(length(dir("src", pattern = "testpkg\\..*")), 1) 13 | expect_true(dir.exists(file.path("src", "rust", "target"))) 14 | 15 | # clean once 16 | clean() 17 | 18 | # we expect an error the second time 19 | expect_error(clean()) 20 | 21 | expect_error(clean(1L)) 22 | expect_error(clean(echo = NULL)) 23 | expect_equal(length(dir("src", pattern = "testpkg\\..*")), 0) 24 | expect_false(dir.exists(file.path("src", "rust", "target"))) 25 | }) 26 | -------------------------------------------------------------------------------- /tests/testthat/test-cran-compliance.R: -------------------------------------------------------------------------------- 1 | test_that("vendor_pkgs() vendors dependencies", { 2 | skip_if_not_installed("usethis") 3 | skip_on_cran() 4 | 5 | path <- local_package("testpkg") 6 | 7 | expect_error(vendor_pkgs(path)) 8 | 9 | # capture setup messages 10 | withr::local_options(usethis.quiet = FALSE) 11 | use_extendr(path, quiet = TRUE) 12 | 13 | package_versions <- vendor_pkgs(path, quiet = TRUE) 14 | expect_snapshot(cat_file("src", "rust", "vendor-config.toml")) 15 | expect_snapshot(package_versions, transform = mask_any_version) 16 | expect_true(file.exists(file.path("src", "rust", "vendor.tar.xz"))) 17 | }) 18 | 19 | 20 | test_that("rextendr passes CRAN checks", { 21 | skip_if_not_installed("usethis") 22 | skip_if_not_installed("rcmdcheck") 23 | skip_on_cran() 24 | 25 | path <- local_package("testpkg") 26 | # write the license file to pass R CMD check 27 | usethis::use_mit_license() 28 | usethis::use_test("dummy", FALSE) 29 | use_extendr() 30 | vendor_pkgs() 31 | document() 32 | 33 | res <- rcmdcheck::rcmdcheck( 34 | env = c("NOT_CRAN" = ""), 35 | args = c("--no-manual", "--no-tests"), 36 | libpath = rev(.libPaths()) 37 | ) 38 | 39 | # --offline flag should be set 40 | expect_true(grepl("--offline", res$install_out)) 41 | # -j 2 flag should be set 42 | expect_true(grepl("-j 2", res$install_out)) 43 | 44 | # "Downloading" should not be present 45 | expect_false(grepl("Downloading", res$install_out)) 46 | 47 | expect_length(res$errors, 0) 48 | expect_length(res$warnings, 0) 49 | }) 50 | -------------------------------------------------------------------------------- /tests/testthat/test-document.R: -------------------------------------------------------------------------------- 1 | test_that("Running `document` after adding multiple files", { 2 | skip_if_not_installed("usethis") 3 | skip_if_not_installed("devtools") 4 | skip_on_cran() 5 | skip_if_cargo_unavailable() 6 | 7 | path <- local_package("testPackage") 8 | use_extendr() 9 | expect_rextendr_error(rextendr::document(), NA) 10 | 11 | file.create(file.path(path, "src/rust/src/a.rs")) 12 | file.create(file.path(path, "src/rust/src/b.rs")) 13 | 14 | expect_rextendr_error(rextendr::document(), NA) 15 | }) 16 | 17 | test_that("Warn if using older rextendr", { 18 | skip_if_not_installed("usethis") 19 | skip_if_not_installed("devtools") 20 | skip_on_cran() 21 | skip_if_cargo_unavailable() 22 | 23 | path <- local_package("futurepkg") 24 | use_extendr() 25 | desc::desc_set(`Config/rextendr/version` = "999.999") 26 | 27 | expect_message(document(quiet = FALSE), "Installed rextendr is older than the version used with this package") 28 | }) 29 | 30 | test_that("Update the Config/rextendr/version field in DESCRIPTION file", { 31 | skip_if_not_installed("usethis") 32 | skip_if_not_installed("devtools") 33 | skip_on_cran() 34 | skip_if_cargo_unavailable() 35 | 36 | path <- local_package("oldpkg") 37 | use_extendr() 38 | desc::desc_set(`Config/rextendr/version` = "0.1") 39 | 40 | expect_message(document(quiet = FALSE), "Setting `Config/rextendr/version` to") 41 | 42 | version_in_desc <- stringi::stri_trim_both(desc::desc_get("Config/rextendr/version", path)[[1]]) 43 | expect_equal(version_in_desc, as.character(packageVersion("rextendr"))) 44 | }) 45 | 46 | test_that("document() warns if NAMESPACE file is malformed", { 47 | skip_if_not_installed("usethis") 48 | skip_if_not_installed("devtools") 49 | skip_on_cran() 50 | skip_if_cargo_unavailable() 51 | 52 | path <- local_package("testPackage") 53 | r"(exportPattern("^[[:alpha:]]+"))" |> brio::write_lines("NAMESPACE") 54 | use_extendr() 55 | expect_warning( 56 | rextendr::document(), 57 | "The 'NAMESPACE' file does not contain the expected `useDynLib` directive." 58 | ) 59 | }) 60 | -------------------------------------------------------------------------------- /tests/testthat/test-eval.R: -------------------------------------------------------------------------------- 1 | test_that("`rust_eval()` works", { 2 | skip_if_cargo_unavailable() 3 | skip_on_cran() 4 | 5 | expect_equal(rust_eval("2 + 2"), 4) 6 | expect_visible(rust_eval("2 + 2")) 7 | 8 | expect_equal(rust_eval("let _ = 2 + 2;"), NULL) 9 | expect_invisible(rust_eval("let _ = 2 + 2;")) 10 | }) 11 | 12 | # Test if multiple Rust chunks can be compiled and then executed 13 | # in the order of compilation. 14 | # 15 | # Generate `n` integers (1..n) and then compile Rust chunks that 16 | # return `n` as `i32`. 17 | # Collect all deferred handles first, then execute them in 18 | # the order of compilation. 19 | # Returned integer values should be identical to the input sequence. 20 | test_that("multiple `rust_eval_deferred()` work correctly", { 21 | skip_if_cargo_unavailable() 22 | skip_on_cran() 23 | 24 | provided_values <- seq_len(5) 25 | deferred_handles <- map( 26 | provided_values, 27 | \(.x) rust_eval_deferred(glue::glue("{.x}i32")) 28 | ) 29 | 30 | obtained_values <- map_int(deferred_handles, rlang::exec) 31 | 32 | testthat::expect_equal( 33 | obtained_values, 34 | provided_values 35 | ) 36 | }) 37 | 38 | 39 | # Test if multiple Rust chunks can be compiled and then executed 40 | # in the reverse order. This ensures that the order of compilation and 41 | # execution do not affect each other. 42 | # 43 | # Generate `n` integers (1..n) and then compile Rust chunks that 44 | # return `n` as `i32`. 45 | # Collect all deferred handles first, then execute them in 46 | # the reverse order. 47 | # Returned integer values should be identical to the reversed input sequence. 48 | test_that("multiple `rust_eval_deferred()` work correctly in reverse order", { 49 | skip_if_cargo_unavailable() 50 | skip_on_cran() 51 | 52 | provided_values <- seq_len(5) 53 | 54 | deferred_handles <- map( 55 | provided_values, 56 | \(.x) rust_eval_deferred(glue::glue("{.x}i32")) 57 | ) 58 | 59 | deferred_handles <- rev(deferred_handles) 60 | 61 | obtained_values <- map_int(deferred_handles, rlang::exec) 62 | 63 | testthat::expect_equal( 64 | obtained_values, 65 | rev(provided_values) 66 | ) 67 | }) 68 | 69 | # Test if the same Rust chunk can be executed multiple times. 70 | # 71 | # After compilation, the Rust chunk can be executed only once, after which 72 | # all associated resources are freed. 73 | # First, compile a simple Rust expression returning an `i32` value. 74 | # Second, execute it once and compare returned value to expected value. 75 | # Third, attempt to execute the same compiled piece of code and 76 | # observe an error. 77 | test_that("`rust_eval_deferred()` disallows multiple executions of the same chunk", { 78 | skip_if_cargo_unavailable() 79 | 80 | handle <- rust_eval_deferred("5i32 + 6i32") 81 | 82 | testthat::expect_equal(handle(), 5L + 6L) 83 | testthat::expect_error( 84 | handle(), 85 | "The Rust code fragment is no longer available for execution." 86 | ) 87 | }) 88 | 89 | # Test if `rust_eval_deferred()` correctly cleans up environment. 90 | # 91 | # Create a simple Rust code chunk and compile it. 92 | # Use attributes to get the name of the Rust (and R wrapper) function and 93 | # the path to the dynamically compiled dll. 94 | # Test if the wrapper is in the environement and the dll is loaded. 95 | # Execute code chunk and verify result. 96 | # Test if the wrapper has been removed and dll unloaded. 97 | test_that("`rust_eval_deferred()` environment cleanup", { 98 | skip_if_cargo_unavailable() 99 | 100 | handle <- rust_eval_deferred("42i32") 101 | fn_name <- attr(handle, "function_name") 102 | dll_path <- attr(handle, "dll_path") 103 | 104 | testthat::expect_true(exists(fn_name)) 105 | dlls <- keep(getLoadedDLLs(), \(.x) .x[["path"]] == dll_path) 106 | testthat::expect_length(dlls, 1L) 107 | 108 | testthat::expect_equal(handle(), 42L) 109 | 110 | testthat::expect_false(exists(fn_name)) 111 | dlls <- keep(getLoadedDLLs(), \(.x) .x[["path"]] == dll_path) 112 | testthat::expect_length(dlls, 0L) 113 | }) 114 | 115 | 116 | # Test that wrapper function names are unique even for identical Rust source 117 | # 118 | # Use the same string to compile two Rust chunks. 119 | # Compare wrapper function names and dll paths (should be unequal). 120 | # Execute both chunks and test results (should be equal). 121 | test_that("`rust_eval_deferred()` generates unique function names", { 122 | skip_if_cargo_unavailable() 123 | 124 | rust_code <- "42f64" 125 | 126 | handle_1 <- rust_eval_deferred(rust_code) 127 | handle_2 <- rust_eval_deferred(rust_code) 128 | 129 | testthat::expect_false( 130 | attr(handle_1, "function_name") == attr(handle_2, "function_name") 131 | ) 132 | 133 | testthat::expect_false( 134 | attr(handle_1, "dll_path") == attr(handle_2, "dll_path") 135 | ) 136 | 137 | testthat::expect_equal(handle_1(), handle_2()) 138 | }) 139 | -------------------------------------------------------------------------------- /tests/testthat/test-extendr_function_options.R: -------------------------------------------------------------------------------- 1 | test_that("`extendr` code is compiled with `either` feature", { 2 | skip_if_cargo_unavailable() 3 | skip_on_cran() 4 | 5 | rust_function( 6 | "fn type_aware_sum(input : Either) -> Either { 7 | match input { 8 | Either::Left(left) => Either::Left(left.iter().sum()), 9 | Either::Right(right) => Either::Right(right.iter().sum()) 10 | } 11 | }", 12 | features = "either", 13 | use_dev_extendr = TRUE 14 | ) 15 | 16 | int_sum <- type_aware_sum(1:5) 17 | 18 | expect_type(int_sum, "integer") 19 | expect_equal(int_sum, 15L) 20 | 21 | dbl_sum <- type_aware_sum(c(1, 2, 3, 4, 5)) 22 | 23 | expect_type(dbl_sum, "double") 24 | expect_equal(dbl_sum, 15) 25 | }) 26 | 27 | test_that("`r_name` option renames R function", { 28 | skip_if_cargo_unavailable() 29 | skip_on_cran() 30 | 31 | rust_function( 32 | "fn func() -> &'static str {\"Modified Name\"}", 33 | extendr_fn_options = list("r_name" = "not_original_name") 34 | ) 35 | 36 | expect_equal(not_original_name(), "Modified Name") 37 | }) 38 | 39 | test_that("`rust_source()` errors if `extendr_fn_options` contains `NULL` value", { 40 | expect_rextendr_error(rust_function("fn func() {}", extendr_fn_options = list("use_rng" = NULL))) 41 | skip_on_cran() 42 | }) 43 | 44 | test_that("`rust_source()` errors if `extendr_fn_options` contains value of the wrong type", { 45 | skip_if_cargo_unavailable() 46 | skip_on_cran() 47 | 48 | # due to the use of purrr here, the error that is emitted is on of class `mutate_error` 49 | # we cannot expect `rextendr_error` from this function. 50 | expect_error(rust_function("fn func() {}", extendr_fn_options = list("use_rng" = 42L))) 51 | }) 52 | 53 | test_that("`rust_source()` errors if `extendr_fn_options` contains option with an invalid name", { 54 | skip_if_cargo_unavailable() 55 | skip_on_cran() 56 | 57 | expect_rextendr_error(rust_function("fn func() {}", extendr_fn_options = list("use try from" = TRUE))) 58 | }) 59 | 60 | test_that("`rust_source()` errors if `extendr_fn_options` contains two invalid options", { 61 | skip_if_cargo_unavailable() 62 | skip_on_cran() 63 | 64 | expect_rextendr_error( 65 | rust_function("fn func() {}", extendr_fn_options = list("use try from" = TRUE, "r_name" = NULL)) 66 | ) 67 | }) 68 | 69 | test_that("`rust_source()` warns if `extendr_fn_options` contains an unknown option", { 70 | skip_if_cargo_unavailable() 71 | skip_on_cran() 72 | 73 | expect_warning( # Unknown option 74 | expect_rextendr_error( # Failed compilation because of the unknonw option 75 | rust_function("fn func() {}", extendr_fn_options = list("unknown_option" = 42L)) 76 | ) 77 | ) 78 | }) 79 | 80 | test_that( 81 | "`rust_source()` does not warn if `extendr_fn_options` contains an unknown option and `use_dev_extendr` is `TRUE`", 82 | { 83 | skip_if_cargo_unavailable() 84 | skip_if_opted_out_of_dev_tests() 85 | skip_on_cran() 86 | 87 | expect_rextendr_error( # Failed compilation because of the unknonw option 88 | rust_function( 89 | code = "fn func() {}", 90 | extendr_fn_options = list("unknown_option" = 42L), 91 | use_dev_extendr = TRUE 92 | ) 93 | ) 94 | } 95 | ) 96 | 97 | 98 | test_that( 99 | "`rust_function()` does not emit any messages when `quiet = TRUE`", 100 | { 101 | skip_if_cargo_unavailable() 102 | skip_on_cran() 103 | 104 | expect_no_message(rust_function(code = "fn func() {}", quiet = TRUE)) 105 | } 106 | ) 107 | -------------------------------------------------------------------------------- /tests/testthat/test-find_extendr.R: -------------------------------------------------------------------------------- 1 | test_that("find_extendr_crate() returns path to Rust crate", { 2 | skip_if_not_installed("usethis") 3 | 4 | path <- local_package("testpkg") 5 | 6 | # capture setup messages 7 | withr::local_options(usethis.quiet = FALSE) 8 | 9 | expect_error(find_extendr_crate(), class = "rextendr_error") 10 | 11 | use_extendr(path, quiet = TRUE) 12 | 13 | rust_folder <- find_extendr_crate() 14 | 15 | expect_true(dir.exists(rust_folder)) 16 | }) 17 | 18 | test_that("find_extendr_manifest() returns path to Cargo manifest", { 19 | skip_if_not_installed("usethis") 20 | 21 | path <- local_package("testpkg") 22 | 23 | # capture setup messages 24 | withr::local_options(usethis.quiet = FALSE) 25 | 26 | expect_error(find_extendr_manifest(), class = "rextendr_error") 27 | 28 | use_extendr(path, quiet = TRUE) 29 | 30 | manifest_path <- find_extendr_manifest() 31 | 32 | expect_true(file.exists(manifest_path)) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/testthat/test-knitr-engine.R: -------------------------------------------------------------------------------- 1 | test_that("knitr-engine works", { 2 | skip_if_cargo_unavailable() 3 | skip_if_not_installed("knitr") 4 | skip_on_cran() 5 | 6 | options <- knitr::opts_chunk$merge(list( 7 | code = "2 + 2", 8 | comment = "##", 9 | eval = TRUE, 10 | echo = TRUE 11 | )) 12 | 13 | expect_equal(eng_extendr(options), "2 + 2\n## [1] 4\n") 14 | 15 | options <- knitr::opts_chunk$merge(list( 16 | code = "rprintln!(\"hello world!\");", 17 | comment = "##", 18 | eval = TRUE, 19 | echo = FALSE 20 | )) 21 | 22 | expect_equal(eng_extendr(options), "## hello world!\n") 23 | }) 24 | 25 | 26 | test_that("Snapshot test of knitr-engine", { 27 | skip_if_cargo_unavailable() 28 | skip_if_not_installed("knitr") 29 | skip_on_cran() 30 | 31 | input <- file.path("../data/test-knitr-engine-source-01.Rmd") 32 | output <- withr::local_file("snapshot_knitr_test.md") 33 | 34 | knitr::knit(input, output) 35 | expect_snapshot(cat_file(output)) 36 | }) 37 | -------------------------------------------------------------------------------- /tests/testthat/test-license_note.R: -------------------------------------------------------------------------------- 1 | test_that("LICENSE.note is generated properly", { 2 | skip_if_not_installed("usethis") 3 | skip_if_cargo_unavailable(c("license", "--help")) 4 | 5 | local_package("testPackage") 6 | 7 | # try running write_license_note() when there is nothing present 8 | dir.create(file.path("src", "rust"), recursive = TRUE) 9 | expect_error(write_license_note()) 10 | 11 | # create license note for extendr package 12 | use_extendr() 13 | write_license_note() 14 | expect_snapshot(cat_file("LICENSE.note")) 15 | expect_error(write_license_note(path = NULL)) 16 | expect_error(write_license_note(force = "yup")) 17 | }) 18 | -------------------------------------------------------------------------------- /tests/testthat/test-make-module-macro.R: -------------------------------------------------------------------------------- 1 | test_that("Module macro generation", { 2 | skip_if_cargo_unavailable() 3 | 4 | rust_src <- r"( 5 | #[extendr] 6 | /* multiline 7 | comment 8 | */fn hello() -> &'static str { 9 | "Hello from Rust!" 10 | } 11 | 12 | #[extendr] 13 | // An awkwardly placed comment 14 | // to verify comments are stripped 15 | fn foo(a: &str, b: i64) { 16 | rprintln!("Data sent to Rust: {}, {}", a, b); 17 | } 18 | 19 | #[extendr] 20 | struct Counter { 21 | n: i32, 22 | } 23 | 24 | #[extendr] 25 | #[allow(dead_code)] 26 | impl Counter { 27 | fn new() -> Self { 28 | Self { n: 0 } 29 | } 30 | 31 | fn increment(&mut self) { 32 | self.n += 1; 33 | } 34 | 35 | fn get(&self) -> i32 { 36 | self.n 37 | } 38 | })" 39 | 40 | expect_equal( 41 | make_module_macro(rust_src), 42 | c( 43 | "extendr_module! {", 44 | "mod rextendr;", 45 | "fn hello;", 46 | "fn foo;", 47 | "impl Counter;", 48 | "}" 49 | ) 50 | ) 51 | 52 | expect_equal( 53 | make_module_macro(rust_src, module_name = "abcd"), 54 | c( 55 | "extendr_module! {", 56 | "mod abcd;", 57 | "fn hello;", 58 | "fn foo;", 59 | "impl Counter;", 60 | "}" 61 | ) 62 | ) 63 | }) 64 | 65 | test_that("Macro generation fails on invalid rust code", { 66 | skip_if_cargo_unavailable() 67 | 68 | expect_rextendr_error( 69 | make_module_macro("#[extendr]\nlet invalid_var = ();"), 70 | "Rust code contains invalid attribute macros." 71 | ) 72 | }) 73 | 74 | 75 | test_that("Macro generation fails on invalid comments in code", { 76 | skip_if_cargo_unavailable() 77 | 78 | expect_rextendr_error( 79 | make_module_macro("/*/*/**/"), 80 | "Malformed comments." 81 | ) 82 | expect_rextendr_error( 83 | make_module_macro("/*/*/**/"), 84 | "delimiters are not equal" 85 | ) 86 | expect_rextendr_error( 87 | make_module_macro("/*/*/**/"), 88 | "Found 3 occurrences" 89 | ) 90 | expect_rextendr_error( 91 | make_module_macro("/*/*/**/"), 92 | "Found 1 occurrence" 93 | ) 94 | 95 | expect_rextendr_error( 96 | make_module_macro("*/ /*"), 97 | "This error may be caused by a code fragment like", 98 | ) 99 | }) 100 | 101 | 102 | test_that("Rust code cleaning", { 103 | skip_if_cargo_unavailable() 104 | 105 | expect_equal( 106 | fill_block_comments(c( 107 | "Nested /*/* this is */ /*commented*/ out */", 108 | "/*/*/**/*/*/comments." 109 | )), 110 | c( 111 | "Nested ", 112 | " comments." 113 | ) 114 | ) 115 | 116 | expect_equal( 117 | remove_line_comments("This is visible //this is not."), 118 | "This is visible " 119 | ) 120 | 121 | expect_equal( 122 | sanitize_rust_code(c( 123 | "/* Comment #1 */", 124 | " // Comment #2", 125 | " ", 126 | " /* Comment #3 // */" 127 | )), 128 | character(0) 129 | ) 130 | }) 131 | 132 | test_that("Rust metadata capturing", { 133 | skip_if_cargo_unavailable() 134 | 135 | expect_equal( 136 | find_extendr_attrs_ids(c( 137 | "#1", 138 | "#[extendr]", 139 | " # 3 ", 140 | " #\t [ \textendr ]", 141 | "#5", 142 | "#[extendr(some_option=true)]", 143 | "#[extendr ( some_option = true ) ]", 144 | "#[extendr()]" 145 | )), 146 | c(2L, 4L, 6L, 7L, 8L) 147 | ) 148 | 149 | expect_equal( 150 | extract_meta("#[extendr] pub \tfn\t test_fn \t() {}"), 151 | data.frame( 152 | match = "fn\t test_fn", 153 | struct = NA_character_, 154 | enum = NA_character_, 155 | fn = "fn", 156 | impl = NA_character_, 157 | lifetime = NA_character_, 158 | name = "test_fn" 159 | ) 160 | ) 161 | 162 | expect_equal( 163 | extract_meta(c( 164 | "#[extendr]", 165 | "pub impl <'a, \t 'b> X {}" 166 | )), 167 | data.frame( 168 | match = "impl <'a, \t 'b> X", 169 | struct = NA_character_, 170 | enum = NA_character_, 171 | fn = NA_character_, 172 | impl = "impl", 173 | lifetime = "'a, \t 'b", 174 | name = "X" 175 | ) 176 | ) 177 | }) 178 | -------------------------------------------------------------------------------- /tests/testthat/test-name-override.R: -------------------------------------------------------------------------------- 1 | test_that("Multiple rust functions with the same name", { 2 | skip_if_cargo_unavailable() 3 | skip_on_cran() 4 | 5 | rust_src_1 <- " 6 | #[extendr] 7 | fn rust_fn_1() -> i32 { 1i32 } 8 | 9 | #[extendr] 10 | fn rust_fn_2() -> i32 { 2i32 } 11 | " 12 | 13 | rust_src_2 <- " 14 | #[extendr] 15 | fn rust_fn_2() -> i32 { 20i32 } 16 | 17 | #[extendr] 18 | fn rust_fn_3() -> i32 { 30i32 } 19 | " 20 | 21 | rust_source( 22 | code = rust_src_1, 23 | quiet = FALSE # , 24 | # toolchain = rust_source_defaults[["toolchain"]], 25 | # patch.crates_io = rust_source_defaults[["patch.crates_io"]] 26 | ) 27 | 28 | # At this point: 29 | # fn1 -> 1 30 | # fn2 -> 2 31 | # fn3 -> (not exported) 32 | 33 | expect_equal(rust_fn_1(), 1L) 34 | expect_equal(rust_fn_2(), 2L) 35 | 36 | rust_source( 37 | code = rust_src_2, 38 | quiet = FALSE # , 39 | # toolchain = rust_source_defaults[["toolchain"]], 40 | # patch.crates_io = rust_source_defaults[["patch.crates_io"]] 41 | ) 42 | 43 | # At this point: 44 | # fn1 -> 1 (unchanged) 45 | # fn2 -> 20 (changed) 46 | # fn3 -> 30 (new function) 47 | 48 | expect_equal(rust_fn_1(), 1L) 49 | expect_equal(rust_fn_2(), 20L) 50 | expect_equal(rust_fn_3(), 30L) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/testthat/test-optional-features.R: -------------------------------------------------------------------------------- 1 | test_that("Feature 'ndarray' is enabled when no extra dependencies are specified", { 2 | skip_if_cargo_unavailable() 3 | skip_on_R42_win() 4 | 5 | input <- file.path("../data/ndarray_example.rs") 6 | rust_source( 7 | file = input, 8 | features = "ndarray" 9 | ) 10 | 11 | data <- matrix(runif(100L), 25) 12 | expected_sum <- sum(data) 13 | actual_sum <- matrix_sum(data) 14 | 15 | expect_equal(actual_sum, expected_sum) 16 | }) 17 | 18 | test_that("Feature 'ndarray' is enabled when 'extendr-api' has features enabled", { 19 | skip_if_cargo_unavailable() 20 | skip_on_R42_win() 21 | 22 | input <- file.path("../data/ndarray_example.rs") 23 | rust_source( 24 | file = input, 25 | features = "ndarray", 26 | extendr_deps = list(`extendr-api` = list(version = "*", features = array("serde"))) 27 | ) 28 | 29 | data <- matrix(runif(100L), 25) 30 | expected_sum <- sum(data) 31 | actual_sum <- matrix_sum(data) 32 | 33 | expect_equal(actual_sum, expected_sum) 34 | }) 35 | 36 | test_that("Enable multiple features simultaneously", { 37 | skip_if_cargo_unavailable() 38 | 39 | rust_function("fn test_multiple_features() {}", features = c("ndarray", "serde", "graphics")) 40 | expect_no_error(test_multiple_features()) 41 | }) 42 | 43 | test_that("Passing integers to `features` results in error", { 44 | skip_if_cargo_unavailable() 45 | 46 | expect_rextendr_error(rust_function("fn test() {}", features = 1:10)) 47 | }) 48 | 49 | test_that("Passing list to `features` results in error", { 50 | skip_if_cargo_unavailable() 51 | 52 | expect_rextendr_error(rust_function("fn test() {}", features = list())) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/testthat/test-pretty_rel_path.R: -------------------------------------------------------------------------------- 1 | # Test if `pretty_rel_path` determines relative paths correctly. 2 | # Edge cases include initiating the search from a directory outside of 3 | # package directory (an ancestor/parent in the hierarchy), and 4 | # from a non-existent/invalid directory (such as `NA` or `""`), 5 | # in which case `pretty_rel_path` should return absolute path of itr 6 | # first argument. 7 | test_that("Find relative path from package root, trivial case", { 8 | skip_if_not_installed("usethis") 9 | 10 | pkg_root <- local_package("testpkg") 11 | use_extendr() 12 | 13 | expect_equal( 14 | pretty_rel_path( 15 | file.path(pkg_root, "R", "extendr-wrappers.R"), 16 | pkg_root 17 | ), 18 | "R/extendr-wrappers.R" 19 | ) 20 | }) 21 | 22 | test_that("Find relative path starting from a subdirectory of package", { 23 | skip_if_not_installed("usethis") 24 | 25 | pkg_root <- local_package("testpkg") 26 | use_extendr() 27 | 28 | expect_equal( 29 | pretty_rel_path( 30 | file.path(pkg_root, "R", "extendr-wrappers.R"), 31 | file.path(pkg_root, "src", "rust", "src", "lib.rs") 32 | ), 33 | "R/extendr-wrappers.R" 34 | ) 35 | }) 36 | 37 | test_that("Find relative path starting outside of package directory, return absolute path", { 38 | skip_if_not_installed("usethis") 39 | 40 | pkg_root <- local_package("testpkg") 41 | use_extendr() 42 | 43 | 44 | expect_equal( 45 | pretty_rel_path( 46 | file.path(pkg_root, "R", "extendr-wrappers.R"), 47 | file.path(pkg_root, "..") 48 | ), 49 | normalizePath( 50 | file.path(pkg_root, "R", "extendr-wrappers.R"), 51 | winslash = "/" 52 | ) 53 | ) 54 | }) 55 | 56 | test_that("Find relative path providing no input for the package directory, return absolute path", { 57 | skip_if_not_installed("usethis") 58 | 59 | pkg_root <- local_package("testpkg") 60 | use_extendr() 61 | 62 | expect_equal( 63 | pretty_rel_path( 64 | file.path(pkg_root, "R", "extendr-wrappers.R"), 65 | "" 66 | ), 67 | normalizePath( 68 | file.path(pkg_root, "R", "extendr-wrappers.R"), 69 | winslash = "/" 70 | ) 71 | ) 72 | }) 73 | 74 | test_that("Find relative path providing NA as input for the package directory, return absolute path", { 75 | skip_if_not_installed("usethis") 76 | 77 | pkg_root <- local_package("testpkg") 78 | use_extendr() 79 | 80 | expect_equal( 81 | pretty_rel_path( 82 | file.path(pkg_root, "R", "extendr-wrappers.R"), 83 | NA_character_ 84 | ), 85 | normalizePath( 86 | file.path(pkg_root, "R", "extendr-wrappers.R"), 87 | winslash = "/" 88 | ) 89 | ) 90 | }) 91 | 92 | test_that( 93 | "Find relative path providing empty character vector as input for the package directory, return absolute path", 94 | { 95 | skip_if_not_installed("usethis") 96 | 97 | pkg_root <- local_package("testpkg") 98 | use_extendr() 99 | 100 | expect_equal( 101 | pretty_rel_path( 102 | file.path(pkg_root, "R", "extendr-wrappers.R"), 103 | character(0) 104 | ), 105 | normalizePath( 106 | file.path(pkg_root, "R", "extendr-wrappers.R"), 107 | winslash = "/" 108 | ) 109 | ) 110 | } 111 | ) 112 | 113 | test_that("Test path to non-existent file", { 114 | skip_if_not_installed("usethis") 115 | 116 | pkg_root <- local_package("testpkg") 117 | use_extendr() 118 | 119 | expect_equal( 120 | pretty_rel_path( 121 | file.path(pkg_root, "A", "B", "C", "D.F"), 122 | pkg_root 123 | ), 124 | "A/B/C/D.F" 125 | ) 126 | }) 127 | -------------------------------------------------------------------------------- /tests/testthat/test-read_cargo_metadata.R: -------------------------------------------------------------------------------- 1 | test_that("read_cargo_metadata() returns crate or workspace metadata", { 2 | skip_if_not_installed("usethis") 3 | 4 | path <- local_package("testpkg") 5 | 6 | # capture setup messages 7 | withr::local_options(usethis.quiet = FALSE) 8 | 9 | use_extendr(path, quiet = TRUE) 10 | 11 | out <- read_cargo_metadata(path) 12 | 13 | expect_type(out, "list") 14 | 15 | expect_equal( 16 | out[["packages"]][["name"]], 17 | "testpkg" 18 | ) 19 | 20 | expect_equal( 21 | out[["packages"]][["version"]], 22 | "0.1.0" 23 | ) 24 | 25 | expect_equal( 26 | out[["packages"]][["dependencies"]][[1]][["name"]], 27 | "extendr-api" 28 | ) 29 | 30 | expect_equal( 31 | out[["workspace_root"]], 32 | normalizePath( 33 | file.path(path, "src", "rust"), 34 | winslash = "\\", 35 | mustWork = FALSE 36 | ) 37 | ) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/testthat/test-rstudio-template.R: -------------------------------------------------------------------------------- 1 | test_that("RStudio template generation is correct", { 2 | pkg_name <- "extendrtest" 3 | tmp <- file.path(tempdir(), pkg_name) 4 | 5 | pkg <- create_extendr_package( 6 | tmp, 7 | roxygen = TRUE, 8 | check_name = FALSE, 9 | crate_name = pkg_name, 10 | lib_name = pkg_name, 11 | edition = "2021" 12 | ) 13 | 14 | expected_files <- c( 15 | "configure", "configure.win", "DESCRIPTION", 16 | "extendrtest.Rproj", "NAMESPACE", "R/extendr-wrappers.R", 17 | "src/entrypoint.c", "src/extendrtest-win.def", 18 | "src/Makevars.in", "src/Makevars.win.in", 19 | "src/rust/Cargo.toml", "src/rust/src/lib.rs", "tools/msrv.R" 20 | ) 21 | 22 | for (file in expected_files) { 23 | expect_true(file.exists(file.path(tmp, file))) 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /tests/testthat/test-rust-sitrep.R: -------------------------------------------------------------------------------- 1 | test_that("`cargo` or `rustup` are not found", { 2 | local_mocked_bindings(try_exec_cmd = function(...) { 3 | NA_character_ 4 | }) 5 | expect_snapshot(rust_sitrep()) 6 | }) 7 | 8 | test_that("`cargo` is found, `rustup` is missing", { 9 | local_mocked_bindings(try_exec_cmd = function(cmd, ...) { 10 | if (cmd == "cargo") { 11 | "cargo 1.0.0 (0000000 0000-00-00)" 12 | } else { 13 | NA_character_ 14 | } 15 | }) 16 | expect_snapshot(rust_sitrep()) 17 | }) 18 | 19 | test_that("`rustup` is found, `cargo` is missing", { 20 | local_mocked_bindings(get_required_target = function(host) "arch-pc-os-tool") 21 | 22 | local_mocked_bindings(try_exec_cmd = function(cmd, args) { 23 | if (cmd == "cargo") { 24 | NA_character_ 25 | } else if (all(args %in% "--version")) { 26 | "rustup 1.0.0 (0000000 0000-00-00)" 27 | } else if (all(args %in% "show")) { 28 | "Default host: arch-pc-os-tool" 29 | } else if (all(args %in% c("toolchain", "list"))) { 30 | "stable-arch-pc-os-tool (default)" 31 | } else if (all(args %in% c("target", "list", "--installed"))) { 32 | "arch-pc-os-tool" 33 | } else { 34 | NA_character_ 35 | } 36 | }) 37 | expect_snapshot(rust_sitrep()) 38 | }) 39 | 40 | test_that("`cargo` and`rustup` are found", { 41 | local_mocked_bindings(get_required_target = function(host) "arch-pc-os-tool") 42 | 43 | local_mocked_bindings(try_exec_cmd = function(cmd, args) { 44 | if (cmd == "cargo") { 45 | "cargo 1.0.0 (0000000 0000-00-00)" 46 | } else if (all(args %in% "--version")) { 47 | "rustup 1.0.0 (0000000 0000-00-00)" 48 | } else if (all(args %in% "show")) { 49 | "Default host: arch-pc-os-tool" 50 | } else if (all(args %in% c("toolchain", "list"))) { 51 | "stable-arch-pc-os-tool (default)" 52 | } else if (all(args %in% c("target", "list", "--installed"))) { 53 | "arch-pc-os-tool" 54 | } else { 55 | NA_character_ 56 | } 57 | }) 58 | expect_snapshot(rust_sitrep()) 59 | }) 60 | 61 | test_that("No toolchains found", { 62 | local_mocked_bindings(try_exec_cmd = function(cmd, args) { 63 | if (cmd == "cargo") { 64 | "cargo 1.0.0 (0000000 0000-00-00)" 65 | } else if (all(args %in% "--version")) { 66 | "rustup 1.0.0 (0000000 0000-00-00)" 67 | } else if (all(args %in% "show")) { 68 | "Default host: arch-pc-os-tool" 69 | } else if (all(args %in% c("toolchain", "list"))) { 70 | character(0) 71 | } else { 72 | NA_character_ 73 | } 74 | }) 75 | expect_snapshot(rust_sitrep()) 76 | }) 77 | 78 | test_that("Wrong toolchain found", { 79 | local_mocked_bindings(try_exec_cmd = function(cmd, args) { 80 | if (cmd == "cargo") { 81 | "cargo 1.0.0 (0000000 0000-00-00)" 82 | } else if (all(args %in% "--version")) { 83 | "rustup 1.0.0 (0000000 0000-00-00)" 84 | } else if (all(args %in% "show")) { 85 | "Default host: arch-pc-os-tool" 86 | } else if (all(args %in% c("toolchain", "list"))) { 87 | "not-a-valid-toolchain" 88 | } else { 89 | NA_character_ 90 | } 91 | }) 92 | expect_snapshot(rust_sitrep()) 93 | }) 94 | 95 | test_that("Wrong toolchain is set as default", { 96 | local_mocked_bindings(try_exec_cmd = function(cmd, args) { 97 | if (cmd == "cargo") { 98 | "cargo 1.0.0 (0000000 0000-00-00)" 99 | } else if (all(args %in% "--version")) { 100 | "rustup 1.0.0 (0000000 0000-00-00)" 101 | } else if (all(args %in% "show")) { 102 | "Default host: arch-pc-os-tool" 103 | } else if (all(args %in% c("toolchain", "list"))) { 104 | c("not-a-valid-toolchain (default)", "stable-arch-pc-os-tool") 105 | } else { 106 | NA_character_ 107 | } 108 | }) 109 | expect_snapshot(rust_sitrep()) 110 | }) 111 | 112 | test_that("Required target is not available", { 113 | local_mocked_bindings(get_required_target = function(host) "required-target") 114 | 115 | local_mocked_bindings(try_exec_cmd = function(cmd, args) { 116 | if (cmd == "cargo") { 117 | "cargo 1.0.0 (0000000 0000-00-00)" 118 | } else if (all(args %in% "--version")) { 119 | "rustup 1.0.0 (0000000 0000-00-00)" 120 | } else if (all(args %in% "show")) { 121 | "Default host: arch-pc-os-tool" 122 | } else if (all(args %in% c("toolchain", "list"))) { 123 | c("not-a-valid-toolchain", "stable-arch-pc-os-tool (default)") 124 | } else if (all(args %in% c("target", "list", "--installed"))) { 125 | c("wrong-target-1", "wrong-target-2") 126 | } else { 127 | NA_character_ 128 | } 129 | }) 130 | expect_snapshot(rust_sitrep()) 131 | }) 132 | 133 | test_that("Detects host when default toolchain is not set", { 134 | 135 | local_mocked_bindings(try_exec_cmd = function(cmd, args) { 136 | if (cmd == "cargo") { 137 | NA_character_ 138 | } else if (cmd == "rustup" & all(args %in% "--version")) { 139 | "rustup 1.0.0 (0000000 0000-00-00)" 140 | } else if (cmd == "rustup" & all(args %in% "show")) { 141 | "Default host: arch-pc-os-tool" 142 | } else if (cmd == "rustc") { 143 | NA_character_ 144 | } else if (all(args %in% c("toolchain", "list"))) { 145 | "stable-arch-pc-os-tool" 146 | } else if (all(args %in% c("target", "list", "--installed"))) { 147 | NA_character_ 148 | } else { 149 | NA_character_ 150 | } 151 | }) 152 | expect_snapshot(rust_sitrep()) 153 | }) 154 | -------------------------------------------------------------------------------- /tests/testthat/test-source.R: -------------------------------------------------------------------------------- 1 | test_that("`rust_source()` works", { 2 | skip_if_cargo_unavailable() 3 | skip_on_cran() 4 | 5 | rust_src <- " 6 | #[extendr] 7 | fn hello() -> &'static str { 8 | \"Hello, this string was created by Rust.\" 9 | } 10 | 11 | #[extendr] 12 | fn add_and_multiply(a: i32, b: i32, c: i32) -> i32 { 13 | c * (a + b) 14 | } 15 | 16 | #[extendr] 17 | fn add(a: i64, b: i64) -> i64 { 18 | a + b 19 | } 20 | 21 | #[extendr] 22 | fn say_nothing() { 23 | 24 | } 25 | " 26 | 27 | rust_source( 28 | code = rust_src, 29 | quiet = FALSE, 30 | cache_build = TRUE # , 31 | ) 32 | 33 | # call `hello()` function from R 34 | # > [1] "Hello, this string was created by Rust." 35 | expect_equal(hello(), "Hello, this string was created by Rust.") 36 | 37 | # call `add()` function from R 38 | expect_equal(add(14, 23), 37) 39 | # > [1] 37 40 | expect_equal(add(17, 42), 17 + 42) 41 | 42 | # This function takes no arguments and invisibly return NULL 43 | expect_null(say_nothing()) 44 | }) 45 | 46 | 47 | test_that("`options` override `toolchain` value in `rust_source`", { 48 | skip_if_cargo_unavailable() 49 | skip_on_cran() 50 | 51 | withr::local_options(rextendr.toolchain = "Non-existent-toolchain") 52 | expect_rextendr_error(rust_function("fn rust_test() {}"), "Rust code could not be compiled successfully. Aborting.") 53 | }) 54 | 55 | test_that("`options` override `patch.crates_io` value in `rust_source`", { 56 | skip_if_cargo_unavailable() 57 | skip_on_cran() 58 | 59 | withr::local_options(rextendr.patch.crates_io = list(`extendr-api` = "-1")) 60 | expect_rextendr_error(rust_function("fn rust_test() {}"), "Rust code could not be compiled successfully. Aborting.") 61 | }) 62 | 63 | 64 | test_that("`options` override `rextendr.extendr_deps` value in `rust_source`", { 65 | skip_if_cargo_unavailable() 66 | skip_on_cran() 67 | 68 | withr::local_options(rextendr.extendr_deps = list(`extendr-api` = "-1")) 69 | expect_rextendr_error(rust_function("fn rust_test() {}"), "Rust code could not be compiled successfully. Aborting.") 70 | }) 71 | 72 | test_that("`rust_source` works even when the PATH is not set correctly, which mainly happens on macOS", { 73 | skip_on_os("windows") # On Windows, we have no concern as the only installation method is the official installer 74 | skip_on_os("linux") # On Linux, `cargo` might be on somewhere like `/usr/bin`, which is hard to eliminate 75 | skip_on_cran() 76 | skip_if_cargo_unavailable() 77 | 78 | # Construct PATH without ~/.cargo/bin 79 | local_path <- Sys.getenv("PATH") 80 | local_path <- stringi::stri_split_fixed(local_path, ":")[[1]] 81 | local_path <- stringi::stri_subset_fixed(local_path, ".cargo/bin", negate = TRUE) 82 | local_path <- glue_collapse(local_path, sep = ":") 83 | 84 | withr::local_envvar(PATH = local_path) 85 | 86 | # confirm cargo is not found 87 | expect_equal(Sys.which("cargo"), c(cargo = "")) 88 | 89 | # confirm `rust_function()` succeeds with a warning 90 | warn_msg <- "Can't find cargo on the PATH. Please review your Rust installation and PATH setups." 91 | expect_error( 92 | expect_warning(rust_function("fn rust_test() {}"), warn_msg), 93 | NULL 94 | ) 95 | }) 96 | 97 | # https://github.com/extendr/rextendr/issues/234 98 | test_that("`rust_code()` can compile code from rust file", { 99 | skip_if_cargo_unavailable() 100 | skip_on_cran() 101 | 102 | input <- file.path("../data/rust_source.rs") 103 | expect_no_error(rust_source(input, module_name = "test_module")) 104 | expect_equal(test_method(), 42L) 105 | }) 106 | 107 | # https://github.com/extendr/rextendr/issues/234 108 | test_that("`rust_code()` can compile code from rust file multiple times", { 109 | skip_if_cargo_unavailable() 110 | skip_on_cran() 111 | 112 | input <- file.path("../data/rust_source.rs") 113 | expect_no_error(rust_source(input, module_name = "test_module")) 114 | expect_no_error(rust_source(input, module_name = "test_module")) 115 | expect_no_error(rust_source(input, module_name = "test_module")) 116 | expect_equal(test_method(), 42L) 117 | }) 118 | 119 | # https://github.com/extendr/rextendr/issues/234 120 | test_that("`rust_code()` can compile code from rust files with identical names", { 121 | skip_if_cargo_unavailable() 122 | skip_on_cran() 123 | 124 | input_1 <- file.path("../data/inner_1/rust_source.rs") 125 | input_2 <- file.path("../data/inner_2/rust_source.rs") 126 | 127 | expect_no_error(rust_source(input_1, module_name = "test_module")) 128 | expect_no_error(rust_source(input_2, module_name = "test_module")) 129 | 130 | expect_equal(test_method_1(), 1L) 131 | expect_equal(test_method_2(), 2L) 132 | }) 133 | 134 | # https://github.com/extendr/rextendr/issues/264 135 | test_that("`rust_source()` should not raise internal error for code without extendr attrs", { 136 | skip_if_cargo_unavailable() 137 | skip_on_cran() 138 | 139 | expect_no_error(rust_source(code = "fn test() {}")) 140 | }) 141 | 142 | # https://github.com/extendr/rextendr/issues/356 143 | test_that("`rust_function()` supports `r#` prefix in rust function names", { 144 | skip_if_cargo_unavailable() 145 | skip_on_cran() 146 | 147 | rust_fn_src <- " 148 | fn r#true() -> &'static str { 149 | \"Specially-named function has been called\" 150 | } 151 | " 152 | 153 | rust_function( 154 | code = rust_fn_src 155 | ) 156 | 157 | expect_equal(true(), "Specially-named function has been called") 158 | }) 159 | -------------------------------------------------------------------------------- /tests/testthat/test-toml.R: -------------------------------------------------------------------------------- 1 | test_that("`toml` is generated correctly", { 2 | # Using cargo's Cargo.toml file for reference 3 | # https://github.com/rust-lang/cargo/blob/master/Cargo.toml 4 | # Testing arrays, names, and nested values 5 | toml <- to_toml( 6 | package = list( 7 | name = "cargo", 8 | version = "0.52.0", 9 | edition = "2018", 10 | authors = c("author_1", "author_2", "author_3"), 11 | license = "MIT OR Apache-2.0", 12 | homepage = "https://crates.io", 13 | repository = "https://github.com/rust-lang/cargo", 14 | documentation = "https://docs.rs/cargo", 15 | readme = "README.md", 16 | description = "Cargo, a package manager for Rust." 17 | ), 18 | dependencies = list( 19 | semver = list(version = "0.10", features = array("serde", 1)), 20 | serde = list(version = "1.0.82", features = array("derive", 1)) 21 | ), 22 | `target.'cfg(target_os = "macos")'.dependencies` = list( 23 | `core-foundation` = list( 24 | version = "0.9.0", 25 | features = array("mac_os_10_7_support", 1) 26 | ) 27 | ), 28 | `target.'cfg(windows)'.dependencies` = list( 29 | miow = "0.3.6", 30 | fwdansi = "1.1.0" 31 | ), 32 | empty_block = NULL, 33 | lib = data.frame( 34 | name = "cargo", 35 | test = FALSE, 36 | doc = FALSE 37 | ), 38 | empty_table = data.frame(), 39 | table_array = data.frame( 40 | x = c(1L, NA_integer_, 2L), 41 | y = c("1", NA_character_, "2") 42 | ), 43 | single_row_array = data.frame(x = 1), 44 | features = list(ndarray = NA), # `NA` gets converted to empty array `[ ]` 45 | .str_as_literal = FALSE 46 | ) 47 | 48 | reference <- c( 49 | "[package]", 50 | "name = \"cargo\"", 51 | "version = \"0.52.0\"", 52 | "edition = \"2018\"", 53 | "authors = [ \"author_1\", \"author_2\", \"author_3\" ]", 54 | "license = \"MIT OR Apache-2.0\"", 55 | "homepage = \"https://crates.io\"", 56 | "repository = \"https://github.com/rust-lang/cargo\"", 57 | "documentation = \"https://docs.rs/cargo\"", 58 | "readme = \"README.md\"", 59 | "description = \"Cargo, a package manager for Rust.\"", 60 | "", 61 | "[dependencies]", 62 | "semver = { version = \"0.10\", features = [ \"serde\" ] }", 63 | "serde = { version = \"1.0.82\", features = [ \"derive\" ] }", 64 | "", 65 | "[target.'cfg(target_os = \"macos\")'.dependencies]", 66 | "core-foundation = { version = \"0.9.0\", features = [ \"mac_os_10_7_support\" ] }", 67 | "", 68 | "[target.'cfg(windows)'.dependencies]", 69 | "miow = \"0.3.6\"", 70 | "fwdansi = \"1.1.0\"", 71 | "", 72 | "[empty_block]", 73 | "", 74 | "[[lib]]", 75 | "name = \"cargo\"", 76 | "test = false", 77 | "doc = false", 78 | "", 79 | "[[empty_table]]", 80 | "", 81 | "[[table_array]]", 82 | "x = 1", 83 | "y = \"1\"", 84 | "[[table_array]]", 85 | "[[table_array]]", 86 | "x = 2", 87 | "y = \"2\"", 88 | "", 89 | "[[single_row_array]]", 90 | "x = 1", 91 | "", 92 | "[features]", 93 | "ndarray = [ ]" 94 | ) 95 | 96 | reference <- glue_collapse(reference, "\n") 97 | 98 | expect_equal(toml, reference) 99 | }) 100 | 101 | 102 | test_that("`toml` does not accept unnamed top-level atomic arguments", { 103 | err <- expect_rextendr_error(to_toml("Invalid String", 1:10)) 104 | expect_equal(unname(err[["message"]]), "Object cannot be serialized.") 105 | }) 106 | -------------------------------------------------------------------------------- /tests/testthat/test-use_crate.R: -------------------------------------------------------------------------------- 1 | test_that("use_crate() adds dependency to package or workspace", { 2 | skip_if_not_installed("usethis") 3 | skip_on_cran() 4 | 5 | path <- local_package("testpkg") 6 | 7 | # capture setup messages 8 | withr::local_options(usethis.quiet = FALSE) 9 | 10 | use_extendr(path, quiet = TRUE) 11 | 12 | use_crate( 13 | "serde", 14 | features = "derive", 15 | version = "1.0.1", 16 | path = path 17 | ) 18 | 19 | metadata <- read_cargo_metadata(path, echo = FALSE) 20 | 21 | dependency <- metadata[["packages"]][["dependencies"]][[1]] 22 | dependency <- dependency[dependency[["name"]] == "serde", ] 23 | 24 | expect_equal(dependency[["name"]], "serde") 25 | expect_equal(dependency[["features"]][[1]], "derive") 26 | expect_equal(dependency[["req"]], "^1.0.1") 27 | 28 | }) 29 | 30 | test_that("use_crate() errors when user passes git and version arguments", { 31 | skip_if_not_installed("usethis") 32 | skip_on_cran() 33 | 34 | path <- local_package("testpkg") 35 | 36 | # capture setup messages 37 | withr::local_options(usethis.quiet = FALSE) 38 | 39 | use_extendr(path, quiet = TRUE) 40 | 41 | fn <- function() { 42 | use_crate( 43 | "serde", 44 | git = "https://github.com/serde-rs/serde", 45 | version = "1.0.1" 46 | ) 47 | } 48 | 49 | expect_error(fn(), class = "rextendr_error") 50 | }) 51 | 52 | test_that("use_crate(optional = TRUE) adds optional dependency", { 53 | skip_if_not_installed("usethis") 54 | skip_on_cran() 55 | 56 | path <- local_package("testpkg") 57 | 58 | # capture setup messages 59 | withr::local_options(usethis.quiet = FALSE) 60 | 61 | use_extendr(path, quiet = TRUE) 62 | 63 | use_crate( 64 | "serde", 65 | optional = TRUE, 66 | path = path 67 | ) 68 | 69 | metadata <- read_cargo_metadata(path) 70 | 71 | dependency <- metadata[["packages"]][["dependencies"]][[1]] 72 | dependency <- dependency[dependency[["name"]] == "serde", ] 73 | 74 | expect_identical(dependency[["optional"]], TRUE) 75 | }) 76 | 77 | test_that("use_crate(git = ) adds dependency with git source", { 78 | skip_if_not_installed("usethis") 79 | skip_on_cran() 80 | 81 | path <- local_package("testpkg") 82 | 83 | # capture setup messages 84 | withr::local_options(usethis.quiet = FALSE) 85 | 86 | use_extendr(path, quiet = TRUE) 87 | 88 | use_crate( 89 | "serde", 90 | git = "https://github.com/serde-rs/serde", 91 | path = path 92 | ) 93 | 94 | metadata <- read_cargo_metadata(path) 95 | 96 | dependency <- metadata[["packages"]][["dependencies"]][[1]] 97 | dependency <- dependency[dependency[["name"]] == "serde", ] 98 | 99 | expect_equal(dependency[["source"]], "git+https://github.com/serde-rs/serde") 100 | }) 101 | -------------------------------------------------------------------------------- /tests/testthat/test-use_dev_extendr.R: -------------------------------------------------------------------------------- 1 | test_that("`use_dev_extendr = TRUE` works together with `features`", { 2 | skip_if_cargo_unavailable() 3 | skip_if_opted_out_of_dev_tests() 4 | 5 | rust_function( 6 | "fn uses_either() -> Either { Either::Left(Rint::from(42i32)) }", 7 | features = "either", 8 | use_dev_extendr = TRUE, 9 | quiet = TRUE # Suppresses warnings while the feature is still experimental 10 | ) 11 | 12 | expect_equal(42L, uses_either()) 13 | }) 14 | -------------------------------------------------------------------------------- /tests/testthat/test-use_msrv.R: -------------------------------------------------------------------------------- 1 | test_that("use_msrv() modifies the MSRV in the DESCRIPTION", { 2 | skip_if_not_installed("usethis") 3 | 4 | path <- local_package("testpkg") 5 | 6 | # capture setup messages 7 | withr::local_options(usethis.quiet = FALSE) 8 | 9 | use_extendr(path, quiet = TRUE) 10 | expect_no_error(use_msrv("1.70", path)) 11 | 12 | d <- desc::desc("DESCRIPTION") 13 | 14 | expect_identical( 15 | "Cargo (Rust's package manager), rustc >= 1.70", 16 | d$get_field("SystemRequirements") 17 | ) 18 | 19 | expect_error(use_msrv("adksfghu", path)) 20 | 21 | expect_error(use_msrv("1.70", path = "../doesntexist")) 22 | 23 | # when overwrite is FALSE and SystemRequirements is already set 24 | expect_message( 25 | use_msrv("1.65", overwrite = FALSE), 26 | "The SystemRequirements field in the " 27 | ) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/testthat/test-use_vscode.R: -------------------------------------------------------------------------------- 1 | test_that("use_vscode creates .vscode and writes settings.json", { 2 | skip_if_not_installed("usethis") 3 | 4 | path <- local_package("testpkg") 5 | withr::local_options(usethis.quiet = FALSE) 6 | 7 | use_vscode(quiet = TRUE, overwrite = TRUE) 8 | 9 | expect_true(dir.exists(".vscode")) 10 | settings <- jsonlite::read_json(file.path(".vscode", "settings.json")) 11 | expect_equal( 12 | settings[["rust-analyzer.linkedProjects"]], 13 | list("${workspaceFolder}/src/rust/Cargo.toml") 14 | ) 15 | }) 16 | 17 | test_that("use_vscode is idempotent and does not duplicate entries", { 18 | skip_if_not_installed("usethis") 19 | 20 | path <- local_package("testpkg") 21 | withr::local_options(usethis.quiet = FALSE) 22 | 23 | use_vscode(quiet = TRUE, overwrite = TRUE) 24 | use_vscode(quiet = TRUE, overwrite = FALSE) 25 | 26 | settings <- jsonlite::read_json(file.path(".vscode", "settings.json")) 27 | expect_equal(length(settings[["rust-analyzer.linkedProjects"]]), 1) 28 | }) 29 | 30 | test_that("overwrite = TRUE replaces existing settings.json", { 31 | skip_if_not_installed("usethis") 32 | 33 | path <- local_package("testpkg") 34 | withr::local_options(usethis.quiet = FALSE) 35 | 36 | use_vscode(quiet = TRUE, overwrite = TRUE) 37 | # corrupt the file 38 | jsonlite::write_json(list(foo = "bar"), file.path(".vscode", "settings.json"), auto_unbox = TRUE) 39 | use_vscode(quiet = TRUE, overwrite = TRUE) 40 | 41 | settings2 <- jsonlite::read_json(file.path(".vscode", "settings.json")) 42 | expect_null(settings2$foo) 43 | expect_equal( 44 | settings2[["rust-analyzer.linkedProjects"]], 45 | list("${workspaceFolder}/src/rust/Cargo.toml") 46 | ) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | test_that("`cargo_command_available()` returns TRUE when `try_exec_cmd()` returns not `NA`", { 2 | local_mocked_bindings(try_exec_cmd = function(...) { 3 | "output" 4 | }) 5 | expect_true(cargo_command_available()) 6 | }) 7 | 8 | test_that("`cargo_command_available()` returns FALSE when `try_exec_cmd()` returns `NA`", { 9 | local_mocked_bindings(try_exec_cmd = function(...) { 10 | NA_character_ 11 | }) 12 | expect_false(cargo_command_available()) 13 | }) 14 | 15 | test_that("`try_exec_cmd()` returns `NA` when command is not available", { 16 | expect_true(is.na(try_exec_cmd("invalidcmdname"))) 17 | }) 18 | 19 | test_that("`try_exec_cmd()` returns stdout when command is available", { 20 | echo <- "This is an echo" 21 | expect_equal(try_exec_cmd("echo", echo), echo) 22 | }) 23 | 24 | test_that("`replace_na()` respects type", { 25 | x <- 1:5 26 | x[2] <- NA 27 | expect_error(replace_na(x, "L")) 28 | }) 29 | 30 | test_that("`replace_na()` replaces with the correct value", { 31 | x <- 1:5 32 | x[2] <- NA_integer_ 33 | expect_identical(replace_na(x, -99L), c(1L, -99L, 3L, 4L, 5L)) 34 | }) 35 | 36 | test_that("is_vscode() returns FALSE when VSCode environment variables are not set", { 37 | withr::with_envvar( 38 | c( 39 | VSCODE_PID = "", 40 | VSCODE_CWD = "", 41 | VSCODE_IPC_HOOK_CLI = "", 42 | TERM_PROGRAM = "" 43 | ), 44 | { 45 | expect_false(is_vscode()) 46 | } 47 | ) 48 | }) 49 | 50 | test_that("is_vscode() returns TRUE when VSCode environment variables are set", { 51 | withr::with_envvar( 52 | c( 53 | VSCODE_PID = "", 54 | VSCODE_CWD = "", 55 | VSCODE_IPC_HOOK_CLI = "", 56 | TERM_PROGRAM = "vscode" 57 | ), 58 | { 59 | expect_true(is_vscode()) 60 | } 61 | ) 62 | }) 63 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/setting_up_rust.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Setting up a Rust build environment" 3 | author: "Claus O. Wilke" 4 | date: "`r Sys.Date()`" 5 | vignette: > 6 | %\VignetteIndexEntry{Setting up Rust} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\usepackage[utf8]{inputenc} 9 | --- 10 | 11 | ```{r setup, include=FALSE} 12 | knitr::opts_chunk$set(echo = TRUE, message = FALSE) 13 | ``` 14 | 15 | Regardless of which operating system you use, we recommend using [rustup](https://rustup.rs/) to install and maintain your Rust toolchain. 16 | On Linux and OS X, you simply run the following command in a shell: 17 | ``` 18 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 19 | ``` 20 | No further action should be needed. 21 | 22 | On Windows, things are a little more involved. First download `rustup‑init.exe` from the [rustup site](https://rustup.rs/) and run it, following the on-screen instructions. Rust may require installation of [VC++ build tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) (more instructions can be found [here](https://learn.microsoft.com/en-us/windows/dev-environment/rust/setup)). 23 | Once installed, execute the following rustup commands: 24 | 25 | ``` 26 | rustup default stable-x86_64-pc-windows-msvc 27 | rustup target add x86_64-pc-windows-gnu 28 | rustup target add i686-pc-windows-gnu 29 | ``` 30 | 31 | Second, install Rtools. The latest installer is available on [CRAN](https://cran.r-project.org/bin/windows/Rtools/). 32 | Alternatively, Rtools can be installed using chocolatey: 33 | ``` 34 | choco install rtools -y 35 | ``` 36 | 37 | Finally, make sure that environment variables are set up correctly. 38 | `R_HOME` should point to the R folder, e.g. `C:\Program Files\R\R-4.1.0` (be careful with spaces in the path). 39 | `RTOOLS40_HOME` should point to the Rtools folder (usually set up automatically by the installer), which is `C:\rtools40` by default. 40 | `PATH` should contain paths to `%R_HOME%\bin` and `%RTOOLS40_HOME%\usr\bin`, as well as cargo, which is found at `%USERPROFILE%\.cargo\bin` if installed using `rustup-init.exe`. 41 | --------------------------------------------------------------------------------