diff --git a/.github/workflows/test-coverage.yaml b/.github/workflows/test-coverage.yaml index 7d32bd81..ba2bb84d 100644 --- a/.github/workflows/test-coverage.yaml +++ b/.github/workflows/test-coverage.yaml @@ -4,14 +4,10 @@ on: push: branches: [main, master] pull_request: - branches: [main, master] - types: - - opened - - reopened - - synchronize - - ready_for_review -name: test-coverage +name: test-coverage.yaml + +permissions: read-all jobs: test-coverage: @@ -21,7 +17,7 @@ jobs: REXTENDR_SKIP_DEV_TESTS: TRUE # TODO: Remove this when extendr/libR-sys issue is resolved steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: r-lib/actions/setup-r@v2 with: @@ -29,28 +25,38 @@ jobs: - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::covr + extra-packages: any::covr, any::xml2 needs: coverage - name: Test coverage run: | - covr::codecov( + cov <- covr::package_coverage( quiet = FALSE, clean = FALSE, - install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") + install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") ) + covr::to_cobertura(cov) shell: Rscript {0} + - uses: codecov/codecov-action@v4 + with: + # Fail if error if not on PR, or if on PR and token is given + fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} + file: ./cobertura.xml + plugin: noop + disable_search: true + token: ${{ secrets.CODECOV_TOKEN }} + - name: Show testthat output if: always() run: | ## -------------------------------------------------------------------- - find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true + find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true shell: bash - name: Upload test results if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage-test-failures path: ${{ runner.temp }}/package diff --git a/.github/workflows/test_pkg_gen.yaml b/.github/workflows/test_pkg_gen.yaml index 31826280..91e9a5c2 100644 --- a/.github/workflows/test_pkg_gen.yaml +++ b/.github/workflows/test_pkg_gen.yaml @@ -85,17 +85,6 @@ jobs: file.path("tests", "testthat", "test-hello.R") ) - # test NOT_CRAN envvar - brio::write_lines( - c( - "if [ \"$NOT_CRAN\" != \"true\" ]; then", - " exit 1", - "fi" - ), - file.path("configure") - ) - Sys.chmod("configure", "0755") - # TODO: allow warnings on oldrel (cf., https://stat.ethz.ch/pipermail/r-package-devel/2023q2/009229.html) if (.Platform$OS.type == "windows" && getRversion() < "4.3.0") { error_on <- "error" diff --git a/DESCRIPTION b/DESCRIPTION index 84f33a87..1c0556e0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: rextendr Title: Call Rust Code from R using the 'extendr' Crate Version: 0.3.1.9001 -Authors@R: +Authors@R: c(person(given = "Claus O.", family = "Wilke", role = "aut", @@ -48,7 +48,6 @@ Imports: jsonlite, pkgbuild (>= 1.4.0), processx, - purrr, rlang (>= 1.0.5), rprojroot, stringi, @@ -57,10 +56,10 @@ Imports: withr Suggests: devtools, + rcmdcheck, knitr, lintr, rmarkdown, - rstudioapi, testthat (>= 3.1.7), usethis VignetteBuilder: diff --git a/NAMESPACE b/NAMESPACE index b04fbc0d..8570dae7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -14,13 +14,13 @@ export(document) export(eng_extendr) export(eng_extendrsrc) export(make_module_macro) +export(read_cargo_metadata) export(register_extendr) export(rust_eval) export(rust_function) export(rust_sitrep) export(rust_source) export(to_toml) -export(use_cran_defaults) export(use_crate) export(use_extendr) export(use_msrv) @@ -30,14 +30,6 @@ importFrom(dplyr,"%>%") importFrom(dplyr,mutate) importFrom(glue,glue) importFrom(glue,glue_collapse) -importFrom(purrr,discard) -importFrom(purrr,every) -importFrom(purrr,flatten_chr) -importFrom(purrr,map) -importFrom(purrr,map2) -importFrom(purrr,map2_chr) -importFrom(purrr,map_if) -importFrom(purrr,map_lgl) importFrom(rlang,"%||%") importFrom(rlang,.data) importFrom(rlang,.env) diff --git a/NEWS.md b/NEWS.md index 61d690cc..73280190 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # rextendr (development version) +* `purrr` has been replaced with [`R/standalone-purrr.R`](https://github.com/r-lib/rlang/blob/main/R/standalone-purrr.R) removing `purrr` from `Imports` +* `document()` will no longer try to save all open files using rstudioapi +* `use_cran_default()` has been removed as the default package template is CRAN compatible +* `use_extendr()` now creates `tools/msrv.R`, `configure` and `configure.win`. These have been moved out of `use_cran_defaults()` +* `Makevars` now prints linked static libraries at compile time by adding `--print=native-static-libs` to `RUSTFLAGS` * `use_extendr()` sets the `DESCRIPTION`'s `SystemRequirements` field according to CRAN policy to `Cargo (Rust's package manager), rustc` (#329) * Introduces new functions `use_cran_defaults()` and `vendor_pkgs()` to ease the publication of extendr-powered packages on CRAN. See the new article _CRAN compliant extendr packages_ on how to use these (#320). * `rust_sitrep()` now better communicates the status of the Rust toolchain and available targets. It also guides the user through necessary installation steps to fix Rust setup (#318). @@ -16,6 +21,8 @@ * Fixed an issue in `rust_source()` family of functions that prevented usage of `r#` escape sequences in Rust function names (#374) * `use_cran_defaults()` now checks the `SystemRequirements` field in the `DESCRIPTION` file for cargo and rustc. It will display installation instructions if either is missing or provide the minimum required version if the installed version is outdated. * Added `use_msrv()` to aid in specifying the minimum supported rust version (MSRV) for an R package +* Added `read_cargo_metadata()` to retrieve Cargo metadata for packages and + workspaces. (#389) # rextend 0.3.1 diff --git a/R/clean.R b/R/clean.R index d59c475a..0a57070f 100644 --- a/R/clean.R +++ b/R/clean.R @@ -4,76 +4,69 @@ #' invokes `cargo clean` to reset cargo target directory #' (found by default at `pkg_root/src/rust/target/`). #' Useful when Rust code should be recompiled from scratch. -#' @param path \[ string \] Path to the package root. +#' +#' @param path character scalar, path to R package root. +#' @param echo logical scalar, should cargo command and outputs be printed to +#' console (default is `TRUE`) +#' +#' @return character vector with names of all deleted files (invisibly). +#' #' @export -clean <- function(path = ".") { - root <- rprojroot::find_package_root_file(path = path) - - rust_folder <- normalizePath( - file.path(root, "src", "rust"), - winslash = "/", - mustWork = FALSE - ) +#' +#' @examples +#' \dontrun{ +#' clean() +#' } +clean <- function(path = ".", echo = TRUE) { + check_string(path, class = "rextendr_error") + check_bool(echo, class = "rextendr_error") - toml_path <- normalizePath( - file.path(rust_folder, "Cargo.toml"), - winslash = "/", - mustWork = FALSE - ) + manifest_path <- find_extendr_manifest(path = path) # Note: This should be adjusted if `TARGET_DIR` changes in `Makevars` - target_dir <- normalizePath( # nolint: object_usage_linter - file.path(rust_folder, "target"), - winslash = "/", - mustWork = FALSE + target_dir <- rprojroot::find_package_root_file( + "src", "rust", "target", + path = path ) - if (!file.exists(toml_path)) { - cli::cli_abort(c( - "Unable to clean binaries.", - "!" = "{.file Cargo.toml} not found in {.path {rust_folder}}.", + if (!dir.exists(target_dir)) { + cli::cli_abort( + c( + "Could not clean binaries.", + "Target directory not found at {.path target_dir}." + ), + call = rlang::caller_call(), class = "rextendr_error" - )) + ) } - cargo_envvars <- get_cargo_envvars() - args <- c( "clean", - glue("--manifest-path={toml_path}"), - glue("--target-dir={target_dir}"), + glue::glue("--manifest-path={manifest_path}"), + glue::glue("--target-dir={target_dir}"), if (tty_has_colors()) { "--color=always" } else { "--color=never" - }, - "--quiet" + } ) - exec_result <- processx::run( - command = "cargo", - args = args, - echo_cmd = FALSE, - windows_verbatim_args = FALSE, - stderr = "|", - stdout = "|", - error_on_status = FALSE, - env = cargo_envvars + + run_cargo( + args, + wd = find_extendr_crate(path = path), + echo = echo ) - if (!isTRUE(exec_result$status == 0)) { - if (!tty_has_colors()) { - err_msg <- cli::ansi_strip(exec_result$stderr) - } else { - err_msg <- exec_result$stderr - } + root <- rprojroot::find_package_root_file(path = path) + + if (!dir.exists(root)) { cli::cli_abort( - c( - "Unable to execute {.code cargo clean}.", - "x" = paste(err_msg, collapse = "\n") - ), - call = caller_env(), + "Could not clean binaries.", + "R package directory not found at {.path root}.", + call = rlang::caller_call(), class = "rextendr_error" ) } + pkgbuild::clean_dll(path = root) } diff --git a/R/cran-compliance.R b/R/cran-compliance.R index 3e1d1a69..68f511d5 100644 --- a/R/cran-compliance.R +++ b/R/cran-compliance.R @@ -1,135 +1,21 @@ -#' Use CRAN compliant defaults -#' -#' Modifies an extendr package to use CRAN compliant settings. -#' -#' @details -#' -#' `use_cran_defaults()` modifies an existing package to provide CRAN complaint -#' settings and files. It creates `tools/msrv.R`, `configure` and `configure.win` files as well as -#' modifies `Makevars` and `Makevars.win` to use required CRAN settings. +#' Vendor Rust dependencies #' #' `vendor_pkgs()` is used to package the dependencies as required by CRAN. #' It executes `cargo vendor` on your behalf creating a `vendor/` directory and a #' compressed `vendor.tar.xz` which will be shipped with package itself. #' If you have modified your dependencies, you will need need to repackage -# the vendored dependencies using `vendor_pkgs()`. +# the vendored dependencies using [`vendor_pkgs()`]. #' #' @inheritParams use_extendr #' @returns #' #' - `vendor_pkgs()` returns a data.frame with two columns `crate` and `version` -#' - `use_cran_defaults()` returns `NULL` and is used solely for its side effects #' #' @examples -#' -#' if (interactive()) { -#' use_cran_defaults() -#' vendor_pkgs() +#' \dontrun{ +#' vendor_pkgs() #' } -#' @name cran -#' @export -use_cran_defaults <- function(path = ".", quiet = FALSE, overwrite = NULL, lib_name = NULL) { - # if not in an interactive session and overwrite is null, set it to false - if (!rlang::is_interactive()) { - overwrite <- overwrite %||% FALSE - } - - # silence output - local_quiet_cli(quiet) - - # find package root - pkg_root <- rprojroot::find_package_root_file(path) - - # set the path for the duration of the function - withr::local_dir(pkg_root) - - if (is.null(lib_name)) { - lib_name <- as_valid_rust_name(pkg_name(path)) - } else if (length(lib_name) > 1) { - cli::cli_abort( - "{.arg lib_name} must be a character scalar", - class = "rextendr_error" - ) - } - - # create tools directory if it does not exist - if (!dir.exists("tools")) { - dir.create("tools") - } - - # add msrv.R template - use_rextendr_template( - "cran/msrv.R", - save_as = file.path("tools", "msrv.R"), - quiet = quiet, - overwrite = overwrite - ) - - # add configure and configure.win templates - use_rextendr_template( - "cran/configure", - save_as = "configure", - quiet = quiet, - overwrite = overwrite, - data = list(lib_name = lib_name) - ) - - # configure needs to be made executable - # ignore for Windows - if (.Platform[["OS.type"]] == "unix") { - Sys.chmod("configure", "0755") - } - - use_rextendr_template( - "cran/configure.win", - save_as = "configure.win", - quiet = quiet, - overwrite = overwrite, - data = list(lib_name = lib_name) - ) - - # use CRAN specific Makevars templates - use_rextendr_template( - "cran/Makevars", - save_as = file.path("src", "Makevars"), - quiet = quiet, - overwrite = overwrite, - data = list(lib_name = lib_name) - ) - - use_rextendr_template( - "cran/Makevars.win", - save_as = file.path("src", "Makevars.win"), - quiet = quiet, - overwrite = overwrite, - data = list(lib_name = lib_name) - ) - - # vendor directory should be ignored by git and R CMD build - if (!rlang::is_installed("usethis")) { - cli::cli_inform( - c( - "!" = "Add {.code ^src/rust/vendor$} to your {.file .Rbuildignore}", - "!" = "Add {.code ^src/rust/vendor$} to your {.file .gitignore}", - "i" = "Install {.pkg usethis} to have this done automatically." - ) - ) - } else { - # vendor folder will be large when expanded and should be ignored - usethis::use_build_ignore( - file.path("src", "rust", "vendor") - ) - - usethis::use_git_ignore( - file.path("src", "rust", "vendor") - ) - } - - invisible(NULL) -} - #' @export -#' @name cran vendor_pkgs <- function(path = ".", quiet = FALSE, overwrite = NULL) { stderr_line_callback <- function(x, proc) { if (!cli::ansi_grepl("To use vendored sources", x) && cli::ansi_nzchar(x)) { @@ -139,12 +25,12 @@ vendor_pkgs <- function(path = ".", quiet = FALSE, overwrite = NULL) { local_quiet_cli(quiet) # get path to rust folder - src_dir <- rprojroot::find_package_root_file(path, "src/rust") + src_dir <- rprojroot::find_package_root_file("src", "rust", path = path) # if `src/rust` does not exist error if (!dir.exists(src_dir)) { cli::cli_abort( - c("{.path src/rust} cannot be found", "i" = "Did you run {.fn use_extendr}?"), + "{.path src/rust} cannot be found. Did you run {.fn use_extendr}?", class = "rextendr_error" ) } @@ -235,3 +121,34 @@ vendor_pkgs <- function(path = ".", quiet = FALSE, overwrite = NULL) { # return packages and versions invisibly invisible(res) } + + +#' CRAN compliant extendr packages +#' +#' R packages developed using extendr are not immediately ready to +#' be published to CRAN. The extendr package template ensures that +#' CRAN publication is (farily) painless. +#' +#' @section CRAN requirements: +#' +#' In order to publish a Rust based package on CRAN it must meet certain +#' requirements. These are: +#' +#' - Rust dependencies are vendored +#' - The package is compiled offline +#' - the `DESCRIPTION` file's `SystemRequirements` field contains `Cargo (Rust's package manager), rustc` +#' +#' The extendr templates handle all of this _except_ vendoring dependencies. +#' This must be done prior to publication using [`vendor_pkgs()`]. +#' +#' In addition, it is important to make sure that CRAN maintainers +#' are aware that the package they are checking contains Rust code. +#' Depending on which and how many crates are used as a dependencies +#' the `vendor.tar.xz` will be larger than a few megabytes. If a +#' built package is larger than 5mbs CRAN may reject the submission. +#' +#' To prevent rejection make a note in your `cran-comments.md` file +#' (create one using [`usethis::use_cran_comments()`]) along the lines of +#' "The package tarball is 6mb because Rust dependencies are vendored within src/rust/vendor.tar.xz which is 5.9mb." +#' @name cran +NULL diff --git a/R/create_extendr_package.R b/R/create_extendr_package.R index e970b9ce..e66dee94 100644 --- a/R/create_extendr_package.R +++ b/R/create_extendr_package.R @@ -25,7 +25,7 @@ create_extendr_package <- function(path, ...) { if (rlang::is_string(x) && nzchar(x)) x else NULL } - args <- purrr::map(args, nullify_empty_string) + args <- map(args, nullify_empty_string) # build package directory, but don't start a new R session with # it as the working directory! i.e., set `open = FALSE` diff --git a/R/eval.R b/R/eval.R index 71ddd299..abd676f1 100644 --- a/R/eval.R +++ b/R/eval.R @@ -119,7 +119,7 @@ fn {fn_name}() -> Result {{ #' `NULL` if no such dll is loaded. #' @noRd find_loaded_dll <- function(name) { - dlls <- purrr::keep(getLoadedDLLs(), ~ .x[["name"]] == name) + dlls <- keep(getLoadedDLLs(), ~ .x[["name"]] == name) if (rlang::is_empty(dlls)) { NULL } else { diff --git a/R/find_exports.R b/R/find_exports.R index 71727e10..6356d0cb 100644 --- a/R/find_exports.R +++ b/R/find_exports.R @@ -8,7 +8,7 @@ find_exports <- function(clean_lns) { return(tibble::tibble(name = character(0), type = character(0), lifetime = character(0))) } - purrr::map2(start, end, ~ extract_meta(clean_lns[.x:.y])) %>% + map2(start, end, ~ extract_meta(clean_lns[.x:.y])) %>% dplyr::bind_rows() %>% dplyr::mutate(type = dplyr::coalesce(.data$impl, .data$fn)) %>% dplyr::select(dplyr::all_of(c("name", "type", "lifetime"))) diff --git a/R/find_extendr.R b/R/find_extendr.R new file mode 100644 index 00000000..033a567c --- /dev/null +++ b/R/find_extendr.R @@ -0,0 +1,61 @@ +#' Get path to Rust crate in R package directory +#' +#' @param path character scalar, the R package directory +#' @param error_call call scalar, from rlang docs: "the defused call with which +#' the function running in the frame was invoked" +#' +#' @return character scalar, path to Rust crate +#' +#' @keywords internal +#' @noRd +find_extendr_crate <- function( + path = ".", + error_call = rlang::caller_call()) { + check_character(path, call = error_call, class = "rextendr_error") + + rust_folder <- rprojroot::find_package_root_file( + "src", "rust", + path = path + ) + + if (!dir.exists(rust_folder)) { + cli::cli_abort( + "Could not find Rust crate at {.path rust_folder}.", + call = error_call, + class = "rextendr_error" + ) + } + + rust_folder +} + +#' Get path to Cargo manifest in R package directory +#' +#' @param path character scalar, the R package directory +#' @param error_call call scalar, from rlang docs: "the defused call with which +#' the function running in the frame was invoked" +#' +#' @return character scalar, path to Cargo manifest +#' +#' @keywords internal +#' @noRd +find_extendr_manifest <- function( + path = ".", + error_call = rlang::caller_call()) { + check_character(path, call = error_call, class = "rextendr_error") + + manifest_path <- rprojroot::find_package_root_file( + "src", "rust", "Cargo.toml", + path = path + ) + + if (!file.exists(manifest_path)) { + cli::cli_abort( + "Could not find Cargo manifest at {.path manifest_path}.", + call = error_call, + class = "rextendr_error" + ) + } + + manifest_path +} diff --git a/R/function_options.R b/R/function_options.R index c5d7c0c1..e3df302c 100644 --- a/R/function_options.R +++ b/R/function_options.R @@ -27,21 +27,21 @@ convert_function_options <- function(options, suppress_warnings) { options_table <- tibble::tibble(Name = rlang::names2(options), Value = unname(options)) %>% dplyr::left_join(extendr_function_config$known_options, by = "Name") %>% dplyr::mutate( - Value = purrr::pmap( + Value = pmap( list(.data$Value, .data$Ptype, .data$Name), ~ if (rlang::is_null(..2)) ..1 else vctrs::vec_cast(..1, ..2, x_arg = ..3) ), ) unknown_option_names <- options_table %>% - dplyr::filter(purrr::map_lgl(.data$Ptype, rlang::is_null)) %>% + dplyr::filter(map_lgl(.data$Ptype, rlang::is_null)) %>% dplyr::pull(.data$Name) invalid_options <- options_table %>% dplyr::mutate( IsNameInvalid = !is_valid_rust_name(.data$Name), - IsValueNull = purrr::map_lgl(.data$Value, rlang::is_null), - IsNotScalar = !.data$IsValueNull & !purrr::map_lgl(.data$Value, vctrs::vec_is, size = 1L) + IsValueNull = map_lgl(.data$Value, rlang::is_null), + IsNotScalar = !.data$IsValueNull & !map_lgl(.data$Value, vctrs::vec_is, size = 1L) ) %>% dplyr::filter( .data$IsNameInvalid | .data$IsValueNull | .data$IsNotScalar @@ -59,7 +59,7 @@ convert_function_options <- function(options, suppress_warnings) { options_table %>% dplyr::transmute( .data$Name, - RustValue = purrr::map_chr(.data$Value, convert_option_to_rust) + RustValue = map_chr(.data$Value, convert_option_to_rust) ) } diff --git a/R/generate_toml.R b/R/generate_toml.R index 39de1845..8010d905 100644 --- a/R/generate_toml.R +++ b/R/generate_toml.R @@ -3,6 +3,17 @@ generate_cargo.toml <- function(libname = "rextendr", patch.crates_io = NULL, extendr_deps = NULL, features = character(0)) { + + # create an empty list if no dependencies are provided + deps <- dependencies %||% list() + # enabled extendr features that we need to impute into all of the + # dependencies + to_impute <- enable_features(extendr_deps, features) + + for (.name in names(to_impute)) { + deps[[.name]] <- to_impute[[.name]] + } + to_toml( package = list( name = libname, @@ -13,10 +24,7 @@ generate_cargo.toml <- function(libname = "rextendr", lib = list( `crate-type` = array("cdylib", 1) ), - dependencies = purrr::list_modify( - dependencies %||% list(), - !!!enable_features(extendr_deps, features) - ), + dependencies = deps, `patch.crates-io` = patch.crates_io, `profile.perf` = list( inherits = "release", diff --git a/R/license_note.R b/R/license_note.R index d3c19b00..214ef961 100644 --- a/R/license_note.R +++ b/R/license_note.R @@ -1,97 +1,140 @@ #' Generate LICENSE.note file. #' -#' LICENSE.note generated by this function contains information about Rust crate dependencies. -#' To use this function, the [cargo-license](https://crates.io/crates/cargo-license) command must be installed. -#' @param force Logical indicating whether to regenerate LICENSE.note if LICENSE.note already exists. -#' @inheritParams register_extendr -#' @return No return value, called for side effects. +#' LICENSE.note generated by this function contains information about all +#' recursive dependencies in Rust crate. +#' +#' @param path character scalar, the R package directory +#' @param quiet logical scalar, whether to signal successful writing of +#' LICENSE.note (default is `FALSE`) +#' @param force logical scalar, whether to regenerate LICENSE.note if +#' LICENSE.note already exists (default is `TRUE`) +#' +#' @return text printed to LICENSE.note (invisibly). +#' #' @export -write_license_note <- function(path = ".", quiet = FALSE, force = TRUE) { - if (!cargo_command_available(c("license", "--help"))) { +#' +#' @examples +#' \dontrun{ +#' write_license_note() +#' } +write_license_note <- function( + path = ".", + quiet = FALSE, + force = TRUE) { + check_string(path, class = "rextendr_error") + check_bool(quiet, class = "rextendr_error") + check_bool(force, class = "rextendr_error") + + outfile <- rprojroot::find_package_root_file( + "LICENSE.note", + path = path + ) + + args <- c( + "metadata", + "--format-version=1" + ) + + metadata <- run_cargo( + args, + wd = find_extendr_crate(path = path), + echo = FALSE, + parse_json = TRUE + ) + + packages <- metadata[["packages"]] + + # did we actually get the recursive dependency metadata we need? + required_variables <- c("name", "repository", "authors", "license", "id") + + packages_exist <- is.data.frame(packages) && + !is.null(packages) && + nrow(packages) > 0 && + all(required_variables %in% names(packages)) + + if (!packages_exist) { cli::cli_abort( - c( - "The {.code cargo license} command is required to run the {.fun write_license_note} function.", - "*" = "Please install cargo-license ({.url https://crates.io/crates/cargo-license}) first.", - i = "Run {.code cargo install cargo-license} from your terminal." - ), + "Unable to write LICENSE.note.", + "Metadata for recursive dependencies not found.", + call = rlang::caller_call(), class = "rextendr_error" ) } - manifest_file <- rprojroot::find_package_root_file("src", "rust", "Cargo.toml", path = path) - outfile <- rprojroot::find_package_root_file("LICENSE.note", path = path) + # exclude current package from LICENSE.note + current_package <- metadata[["resolve"]][["root"]] + + current_package_exists <- length(current_package) == 1 && + is.character(current_package) && + !is.null(current_package) - if (!isTRUE(force) && file.exists(outfile)) { + if (!current_package_exists) { cli::cli_abort( - c( - "LICENSE.note already exists.", - "If you want to regenerate LICENSE.note, set `force = TRUE` to {.fun write_license_note}." - ), + "Unable to write LICENSE.note.", + "Failed to identify current Rust crate.", + call = rlang::caller_call(), class = "rextendr_error" ) } - list_license <- processx::run( - "cargo", - c( - "license", - "--authors", - "--json", - "--avoid-build-deps", - "--avoid-dev-deps", - "--manifest-path", manifest_file - ) - )$stdout %>% - jsonlite::parse_json() - - package_names <- processx::run( - "cargo", - c( - "metadata", - "--no-deps", - "--format-version", "1", - "--manifest-path", manifest_file - ) - )$stdout %>% - jsonlite::parse_json() %>% - purrr::pluck("packages") %>% - purrr::map_chr("name") - - .prep_authors <- function(authors, package) { - ifelse(!is.null(authors), authors, paste0(package, " authors")) %>% - stringi::stri_replace_all_regex(r"(\ <.+?>)", "") %>% - stringi::stri_replace_all_regex(r"(\|)", ", ") - } + packages <- packages[packages[["id"]] != current_package, ] + + # replace missing values + packages[["respository"]] <- replace_na( + packages[["repository"]], + "unknown" + ) + + packages[["licenses"]] <- replace_na( + packages[["repository"]], + "not provided" + ) + + # remove email addresses and special characters and combine all authors + # of a crate into a single character scalar + packages[["authors"]] <- unlist(Map( + prep_authors, + packages[["authors"]], + packages[["name"]] + )) separator <- "-------------------------------------------------------------" note_header <- paste0( - "The binary compiled from the source code of this package contains the following Rust crates:\n", + "The binary compiled from the source code of this package ", + "contains the following Rust crates:\n", "\n", "\n", separator ) - note_body <- list_license %>% - purrr::discard(function(x) x$name %in% package_names) %>% - purrr::map_chr( - function(x) { - paste0( - "\n", - "Name: ", x$name, "\n", - "Repository: ", x$repository, "\n", - "Authors: ", .prep_authors(x$authors, x$name), "\n", - "License: ", x$license, "\n", - "\n", - separator - ) - } - ) + note_body <- paste0( + "\n", + "Name: ", packages[["name"]], "\n", + "Repository: ", packages[["repository"]], "\n", + "Authors: ", packages[["authors"]], "\n", + "License: ", packages[["license"]], "\n", + "\n", + separator + ) write_file( text = c(note_header, note_body), path = outfile, search_root_from = path, - quiet = quiet + quiet = quiet, + overwrite = force + ) +} + +prep_authors <- function(authors, package) { + authors <- ifelse( + is.na(authors), + paste0(package, " authors"), + authors ) + + authors <- stringi::stri_replace_all_regex(authors, r"(\ <.+?>)", "") + + paste0(authors, collapse = ", ") } diff --git a/R/read_cargo_metadata.R b/R/read_cargo_metadata.R new file mode 100644 index 00000000..e3c9ffb7 --- /dev/null +++ b/R/read_cargo_metadata.R @@ -0,0 +1,60 @@ +#' Retrieve metadata for packages and workspaces +#' +#' @param path character scalar, the R package directory +#' @param dependencies Default `FALSE`. A logical scalar, whether to include +#' all recursive dependencies in stdout. +#' @param echo Default `FALSE`. A logical scalar, should cargo command and +#' outputs be printed to the console. +#' +#' @details +#' For more details, see +#' \href{https://doc.rust-lang.org/cargo/commands/cargo-metadata.html}{Cargo docs} +#' for `cargo-metadata`. See especially "JSON Format" to get a sense of what you +#' can expect to find in the returned list. +#' +#' @returns +#' A `list` including the following elements: +#' - `packages` +#' - `workspace_members` +#' - `workspace_default_members` +#' - `resolve` +#' - `target_directory` +#' - `version` +#' - `workspace_root` +#' - `metadata` +#' +#' @export +#' +#' @examples +#' \dontrun{ +#' read_cargo_metadata() +#' } +#' +read_cargo_metadata <- function( + path = ".", + dependencies = FALSE, + echo = FALSE) { + check_string(path, class = "rextendr_error") + check_bool(dependencies, class = "rextendr_error") + check_bool(echo, class = "rextendr_error") + + args <- c( + "metadata", + "--format-version=1", + if (!dependencies) { + "--no-deps" + }, + if (tty_has_colors()) { + "--color=always" + } else { + "--color=never" + } + ) + + run_cargo( + args, + wd = find_extendr_crate(path = path), + echo = echo, + parse_json = TRUE + ) +} diff --git a/R/rextendr.R b/R/rextendr.R index 09aee13d..f8b26d4b 100644 --- a/R/rextendr.R +++ b/R/rextendr.R @@ -7,7 +7,6 @@ "_PACKAGE" #' @importFrom dplyr mutate %>% -#' @importFrom purrr map2 map2_chr map_lgl flatten_chr map_if every map discard #' @importFrom glue glue glue_collapse #' @importFrom rlang dots_list names2 as_function is_missing is_atomic is_null #' @importFrom rlang is_na .data .env caller_env as_name as_label enquo %||% diff --git a/R/rextendr_document.R b/R/rextendr_document.R index 85393b82..9883945d 100644 --- a/R/rextendr_document.R +++ b/R/rextendr_document.R @@ -10,8 +10,6 @@ #' @return No return value, called for side effects. #' @export document <- function(pkg = ".", quiet = FALSE, roclets = NULL) { - try_save_all(quiet = quiet) - withr::local_envvar(devtools::r_env_vars()) register_extendr(path = pkg, quiet = quiet) diff --git a/R/run_cargo.R b/R/run_cargo.R new file mode 100644 index 00000000..1db8ed13 --- /dev/null +++ b/R/run_cargo.R @@ -0,0 +1,80 @@ +#' Run Cargo subcommands +#' +#' This internal function allows us to maintain consistent specifications for +#' `processx::run()` everywhere it uses. +#' +#' @param args character vector, the Cargo subcommand and flags to be executed. +#' @param wd character scalar, location of the Rust crate, (default is +#' `find_extendr_crate()`). +#' @param error_on_status Default `TRUE`. A logical scalar, whether to error on a non-zero exist status. +#' @param echo_cmd Default `TRUE`. A logical scalar, whether to print Cargo subcommand and flags +#' to the console. +#' @param echo Default `TRUE`. Alogical scalar, whether to print standard output and error to the +#' console. +#' @param env character vector, environment variables of the child process. +#' @param parse_json Default `FALSE`. A logical scalar, whether to parse JSON-structured standard +#' output using [`jsonlite::parse_json()`] with `simplifyDataFrame = TRUE`. +#' @param error_call Default [`rlang::caller_call()`]. The defused call with which +#' the function running in the frame was invoked. +#' @param ... additional arguments passed to [`processx::run()`]. +#' @returns +#' A list with elements `status`, `stdout`, `stderr`, and `timeout`. +#' See [`processx::run()`]. If `parse_json = TRUE`, result of parsing +#' JSON-structured standard output. +#' +#' @keywords internal +#' @noRd +run_cargo <- function( + args, + wd = find_extendr_crate(), + error_on_status = TRUE, + echo = TRUE, + env = get_cargo_envvars(), + parse_json = FALSE, + error_call = rlang::caller_call(), + ... +) { + check_character(args, call = error_call, class = "rextendr_error") + check_string(wd, call = error_call, class = "rextendr_error") + check_bool(error_on_status, call = error_call, class = "rextendr_error") + check_bool(echo, call = error_call, class = "rextendr_error") + check_character(env, call = error_call, class = "rextendr_error") + check_bool(parse_json, call = error_call, class = "rextendr_error") + + out <- processx::run( + command = "cargo", + args = args, + error_on_status = error_on_status, + wd = wd, + echo_cmd = echo, + echo = echo, + env = env, + ... + ) + + stdout <- out[["stdout"]] + + if (length(stdout) != 1L || !is.character(stdout) || is.null(stdout)) { + cli::cli_abort( + "{.code cargo paste(args, collapse = ' ')} failed to return stdout.", + call = error_call, + class = "rextendr_error" + ) + } + + if (parse_json) { + res <- rlang::try_fetch( + jsonlite::parse_json(stdout, simplifyDataFrame = TRUE), + error = function(cnd) { + cli::cli_abort( + c("Failed to {.code stdout} as json:", " " = "{stdout}"), + parent = cnd, + class = "rextendr_error" + ) + } + ) + return(res) + } + + out +} diff --git a/R/sanitize_code.R b/R/sanitize_code.R index 368e1d87..cfcb88fa 100644 --- a/R/sanitize_code.R +++ b/R/sanitize_code.R @@ -35,8 +35,8 @@ fill_block_comments <- function(lns, fill_with = " ") { # nolint: object_usage_l # A sorted DF having `start`, `end`, and `type` comment_syms <- locations %>% - purrr::map(tibble::as_tibble) %>% - purrr::imap( + map(tibble::as_tibble) %>% + imap( ~ dplyr::mutate( .x, type = dplyr::if_else(.y == 1L, "open", "close") @@ -136,20 +136,16 @@ fill_block_comments <- function(lns, fill_with = " ") { # nolint: object_usage_l # of the same length -- this is needed to preserve line length # and previously computed positions, and it does not affect # parsing at later stages. - result <- purrr::reduce2( - to_replace[["start_open"]], - to_replace[["end_close"]], - function(ln, from, to) { - stringi::stri_sub( - ln, - from, - to, - ) <- strrep(fill_with, to - from + 1L) - ln - }, - .init = lns - ) + .open <- to_replace[["start_open"]] + .close <- to_replace[["end_close"]] + gap_size <- (.close - .open) + 1 + result <- stringi::stri_sub_replace_all( + lns, + .open, + .close, + replacement = strrep(fill_with, gap_size) + ) result <- stringi::stri_split_lines(result, omit_empty = TRUE)[[1]] result diff --git a/R/source.R b/R/source.R index 8ec5adb5..d4ad449a 100644 --- a/R/source.R +++ b/R/source.R @@ -404,10 +404,10 @@ invoke_cargo <- function(toolchain, specific_target, dir, profile, gather_cargo_output <- function(json_output, level, tty_has_colors) { rendered_output <- json_output %>% - purrr::keep( + keep( ~ .x$reason == "compiler-message" && .x$message$level == level ) %>% - purrr::map_chr(~ .x$message$rendered) + map_chr(~ .x$message$rendered) if (!tty_has_colors) { rendered_output <- cli::ansi_strip(rendered_output) @@ -433,7 +433,7 @@ gather_cargo_output <- function(json_output, level, tty_has_colors) { #' @param call Caller environment used for error message formatting. #' @noRd check_cargo_output <- function(compilation_result, message_buffer, tty_has_colors, quiet, call = caller_env()) { - cargo_output <- purrr::map( + cargo_output <- map( message_buffer, jsonlite::parse_json ) @@ -445,7 +445,7 @@ check_cargo_output <- function(compilation_result, message_buffer, tty_has_color "error", tty_has_colors ) %>% - purrr::map_chr( + map_chr( cli::format_inline, keep_whitespace = TRUE ) %>% diff --git a/R/standalone-purrr.R b/R/standalone-purrr.R new file mode 100644 index 00000000..f9322322 --- /dev/null +++ b/R/standalone-purrr.R @@ -0,0 +1,236 @@ +# --- +# repo: r-lib/rlang +# file: standalone-purrr.R +# last-updated: 2023-02-23 +# license: https://unlicense.org +# imports: rlang +# --- +# +# This file provides a minimal shim to provide a purrr-like API on top of +# base R functions. They are not drop-in replacements but allow a similar style +# of programming. +# +# ## Changelog +# +# 2023-02-23: +# * Added `list_c()` +# +# 2022-06-07: +# * `transpose()` is now more consistent with purrr when inner names +# are not congruent (#1346). +# +# 2021-12-15: +# * `transpose()` now supports empty lists. +# +# 2021-05-21: +# * Fixed "object `x` not found" error in `imap()` (@mgirlich) +# +# 2020-04-14: +# * Removed `pluck*()` functions +# * Removed `*_cpl()` functions +# * Used `as_function()` to allow use of `~` +# * Used `.` prefix for helpers +# +# nocov start + +map <- function(.x, .f, ...) { + .f <- rlang::as_function(.f, env = rlang::global_env()) + lapply(.x, .f, ...) +} +walk <- function(.x, .f, ...) { + map(.x, .f, ...) + invisible(.x) +} + +map_lgl <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, logical(1), ...) +} +map_int <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, integer(1), ...) +} +map_dbl <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, double(1), ...) +} +map_chr <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, character(1), ...) +} +.rlang_purrr_map_mold <- function(.x, .f, .mold, ...) { + .f <- rlang::as_function(.f, env = rlang::global_env()) + out <- vapply(.x, .f, .mold, ..., USE.NAMES = FALSE) + names(out) <- names(.x) + out +} + +map2 <- function(.x, .y, .f, ...) { + .f <- rlang::as_function(.f, env = rlang::global_env()) + out <- mapply(.f, .x, .y, MoreArgs = list(...), SIMPLIFY = FALSE) + if (length(out) == length(.x)) { + rlang::set_names(out, names(.x)) + } else { + rlang::set_names(out, NULL) + } +} +map2_lgl <- function(.x, .y, .f, ...) { + as.vector(map2(.x, .y, .f, ...), "logical") +} +map2_int <- function(.x, .y, .f, ...) { + as.vector(map2(.x, .y, .f, ...), "integer") +} +map2_dbl <- function(.x, .y, .f, ...) { + as.vector(map2(.x, .y, .f, ...), "double") +} +map2_chr <- function(.x, .y, .f, ...) { + as.vector(map2(.x, .y, .f, ...), "character") +} +imap <- function(.x, .f, ...) { + map2(.x, names(.x) %||% seq_along(.x), .f, ...) +} + +pmap <- function(.l, .f, ...) { + .f <- rlang::as_function(.f) + args <- .rlang_purrr_args_recycle(.l) + do.call("mapply", c( + FUN = list(quote(.f)), + args, MoreArgs = quote(list(...)), + SIMPLIFY = FALSE, USE.NAMES = FALSE + )) +} +.rlang_purrr_args_recycle <- function(args) { + lengths <- map_int(args, length) + n <- max(lengths) + + stopifnot(all(lengths == 1L | lengths == n)) + to_recycle <- lengths == 1L + args[to_recycle] <- map(args[to_recycle], function(x) rep.int(x, n)) + + args +} + +keep <- function(.x, .f, ...) { + .x[.rlang_purrr_probe(.x, .f, ...)] +} +discard <- function(.x, .p, ...) { + sel <- .rlang_purrr_probe(.x, .p, ...) + .x[is.na(sel) | !sel] +} +map_if <- function(.x, .p, .f, ...) { + matches <- .rlang_purrr_probe(.x, .p) + .x[matches] <- map(.x[matches], .f, ...) + .x +} +.rlang_purrr_probe <- function(.x, .p, ...) { + if (rlang::is_logical(.p)) { + stopifnot(length(.p) == length(.x)) + .p + } else { + .p <- rlang::as_function(.p, env = rlang::global_env()) + map_lgl(.x, .p, ...) + } +} + +compact <- function(.x) { + Filter(length, .x) +} + +transpose <- function(.l) { + if (!length(.l)) { + return(.l) + } + + inner_names <- names(.l[[1]]) + + if (is.null(inner_names)) { + fields <- seq_along(.l[[1]]) + } else { + fields <- rlang::set_names(inner_names) + .l <- map(.l, function(x) { + if (is.null(names(x))) { + rlang::set_names(x, inner_names) + } else { + x + } + }) + } + + # This way missing fields are subsetted as `NULL` instead of causing + # an error + .l <- map(.l, as.list) + + map(fields, function(i) { + map(.l, .subset2, i) + }) +} + +every <- function(.x, .p, ...) { + .p <- rlang::as_function(.p, env = rlang::global_env()) + + for (i in seq_along(.x)) { + if (!rlang::is_true(.p(.x[[i]], ...))) return(FALSE) + } + TRUE +} +some <- function(.x, .p, ...) { + .p <- rlang::as_function(.p, env = rlang::global_env()) + + for (i in seq_along(.x)) { + if (rlang::is_true(.p(.x[[i]], ...))) return(TRUE) + } + FALSE +} +negate <- function(.p) { + .p <- rlang::as_function(.p, env = rlang::global_env()) + function(...) !.p(...) +} + +reduce <- function(.x, .f, ..., .init) { + f <- function(x, y) .f(x, y, ...) + Reduce(f, .x, init = .init) +} +reduce_right <- function(.x, .f, ..., .init) { + f <- function(x, y) .f(y, x, ...) + Reduce(f, .x, init = .init, right = TRUE) +} +accumulate <- function(.x, .f, ..., .init) { + f <- function(x, y) .f(x, y, ...) + Reduce(f, .x, init = .init, accumulate = TRUE) +} +accumulate_right <- function(.x, .f, ..., .init) { + f <- function(x, y) .f(y, x, ...) + Reduce(f, .x, init = .init, right = TRUE, accumulate = TRUE) +} + +detect <- function(.x, .f, ..., .right = FALSE, .p = rlang::is_true) { + .p <- rlang::as_function(.p, env = rlang::global_env()) + .f <- rlang::as_function(.f, env = rlang::global_env()) + + for (i in .rlang_purrr_index(.x, .right)) { + if (.p(.f(.x[[i]], ...))) { + return(.x[[i]]) + } + } + NULL +} +detect_index <- function(.x, .f, ..., .right = FALSE, .p = rlang::is_true) { + .p <- rlang::as_function(.p, env = rlang::global_env()) + .f <- rlang::as_function(.f, env = rlang::global_env()) + + for (i in .rlang_purrr_index(.x, .right)) { + if (.p(.f(.x[[i]], ...))) { + return(i) + } + } + 0L +} +.rlang_purrr_index <- function(x, right = FALSE) { + idx <- seq_along(x) + if (right) { + idx <- rev(idx) + } + idx +} + +list_c <- function(x) { + rlang::inject(c(!!!x)) +} + +# nocov end diff --git a/R/toml_serialization.R b/R/toml_serialization.R index 3f664c33..b6211912 100644 --- a/R/toml_serialization.R +++ b/R/toml_serialization.R @@ -41,7 +41,7 @@ to_toml <- function(..., names <- names2(args) # We disallow unnamed top-level atomic arguments - invalid <- which(purrr::map2_lgl(names, args, ~ !nzchar(.x) && is.atomic(.y))) + invalid <- which(map2_lgl(names, args, ~ !nzchar(.x) && is.atomic(.y))) # If such args found, display an error message if (length(invalid) > 0) { @@ -55,7 +55,7 @@ to_toml <- function(..., ) } - tables <- map2_chr(names, args, function(nm, a) { + tables <- map2(names, args, function(nm, a) { header <- make_header(nm, a) body <- format_toml( a, @@ -75,6 +75,7 @@ to_toml <- function(..., # remove them by `c()` first, and then concatenate by "\n" if both exists glue_collapse(c(header, body), "\n") }) + glue_collapse(tables, "\n\n") } @@ -97,7 +98,7 @@ simplify_row <- function(row) { result <- map_if( row, ~ is.list(.x) && all(!nzchar(names2(.x))), - ~ .x[[1]], + ~ .x[1], .else = ~.x ) discard( @@ -134,20 +135,21 @@ format_toml.data.frame <- function(x, if (length(item) == 0L) { result <- character(0) } else { + result <- format_toml( - item, + as.list(item), ..., .top_level = TRUE ) } if (!is_atomic(result)) { - result <- flatten_chr(result) + result <- list_c(result) } c(header, result) } ) - flatten_chr(result) + list_c(result) } # This handles missing args @@ -200,7 +202,7 @@ format_toml_atomic <- function(x, if (len == 0L) { "[ ]" } else { - formatter <- as_function(.formatter) + formatter <- rlang::as_function(.formatter) items <- glue_collapse(formatter(x, ...), ", ") if (len > 1L || !is.null(dims)) { items <- glue("[ {items} ]") @@ -296,7 +298,7 @@ format_toml.list <- function(x, ..., .top_level = FALSE) { result <- glue("{{ {paste0(result, collapse = \", \")} }}") } if (!is_atomic(result)) { - result <- flatten_chr(result) + result <- list_c(result) } # Ensure type-stability as.character(result) diff --git a/R/track_rust_source.R b/R/track_rust_source.R index dd96b333..dca7e967 100644 --- a/R/track_rust_source.R +++ b/R/track_rust_source.R @@ -87,7 +87,7 @@ pretty_rel_single_path <- function(path, search_from = ".") { #' #' @noRd pretty_rel_path <- function(path, search_from = ".") { - purrr::map_chr(path, pretty_rel_single_path, search_from = search_from) + map_chr(path, pretty_rel_single_path, search_from = search_from) } get_library_path <- function(path = ".") { diff --git a/R/try_save_all.R b/R/try_save_all.R deleted file mode 100644 index 220f653a..00000000 --- a/R/try_save_all.R +++ /dev/null @@ -1,15 +0,0 @@ -#' Try to save open files if \pkg{rextendr} is called from an IDE. -#' -#' Uses rstudio API (if available) to save modified files. -#' Improves package development experience within RStudio. -#' @param quiet Logical scalar indicating whether the output should be quiet (`TRUE`) -#' or verbose (`FALSE`). -#' @noRd -try_save_all <- function(quiet = FALSE) { - if (requireNamespace("rstudioapi", quietly = TRUE) && rstudioapi::hasFun("documentSaveAll")) { - rstudioapi::documentSaveAll() - if (!isTRUE(quiet)) { - cli::cli_alert_success("Saving changes in the open files.") - } - } -} diff --git a/R/use_crate.R b/R/use_crate.R index cea33920..f249c4e3 100644 --- a/R/use_crate.R +++ b/R/use_crate.R @@ -10,13 +10,15 @@ #' @param optional boolean scalar, whether to mark the dependency as optional #' (FALSE by default) #' @param path character scalar, the package directory +#' @param echo logical scalar, should cargo command and outputs be printed to +#' console (default is TRUE) #' #' @details #' For more details regarding these and other options, see the #' \href{https://doc.rust-lang.org/cargo/commands/cargo-add.html}{Cargo docs} #' for `cargo-add`. #' -#' @return `NULL`, invisibly +#' @return `NULL` (invisibly) #' #' @export #' @@ -43,52 +45,61 @@ use_crate <- function( git = NULL, version = NULL, optional = FALSE, - path = ".") { - # check args - check_string(crate) - check_character(features, allow_null = TRUE) - check_string(git, allow_null = TRUE) - check_string(version, allow_null = TRUE) - check_bool(optional) - check_string(path) + path = ".", + echo = TRUE) { + check_string(crate, class = "rextendr_error") + check_character(features, allow_null = TRUE, class = "rextendr_error") + check_string(git, allow_null = TRUE, class = "rextendr_error") + check_string(version, allow_null = TRUE, class = "rextendr_error") + check_bool(optional, class = "rextendr_error") + check_string(path, class = "rextendr_error") + check_bool(echo, class = "rextendr_error") + + if (!is.null(version) && !is.null(git)) { + cli::cli_abort( + "Cannot specify a git URL ('{git}') with a version ('{version}').", + class = "rextendr_error" + ) + } if (!is.null(version)) { crate <- paste0(crate, "@", version) } - # combine main options - cargo_add_opts <- list( - "--features" = paste0(features, collapse = " "), - "--git" = git, - "--optional" = tolower(as.character(optional)) - ) - - # clear empty options - cargo_add_opts <- purrr::discard(cargo_add_opts, rlang::is_empty) + if (!is.null(features)) { + features <- c( + "--features", + paste(crate, features, sep = "/", collapse = ",") + ) + } - # combine option names and values into single strings - adtl_args <- unname(purrr::imap_chr( - cargo_add_opts, - function(x, i) { - paste(i, paste0(x, collapse = " ")) - } - )) + if (!is.null(git)) { + git <- c("--git", git) + } - # get rust directory in project folder - root <- rprojroot::find_package_root_file(path = path) + if (optional) { + optional <- "--optional" + } else { + optional <- NULL + } - rust_folder <- normalizePath( - file.path(root, "src", "rust"), - winslash = "/", - mustWork = FALSE + args <- c( + "add", + crate, + features, + git, + optional, + if (tty_has_colors()) { + "--color=always" + } else { + "--color=never" + } ) - # run the commmand - processx::run( - "cargo", - c("add", crate, adtl_args), - echo_cmd = TRUE, - wd = rust_folder + run_cargo( + args, + wd = find_extendr_crate(path = path), + echo = echo ) invisible() diff --git a/R/use_extendr.R b/R/use_extendr.R index e0694db3..e33be192 100644 --- a/R/use_extendr.R +++ b/R/use_extendr.R @@ -101,16 +101,16 @@ use_extendr <- function(path = ".", ) use_rextendr_template( - "Makevars", - save_as = file.path("src", "Makevars"), + "Makevars.in", + save_as = file.path("src", "Makevars.in"), quiet = quiet, overwrite = overwrite, data = list(lib_name = lib_name) ) use_rextendr_template( - "Makevars.win", - save_as = file.path("src", "Makevars.win"), + "Makevars.win.in", + save_as = file.path("src", "Makevars.win.in"), quiet = quiet, overwrite = overwrite, data = list(lib_name = lib_name) @@ -131,8 +131,6 @@ use_extendr <- function(path = ".", overwrite = overwrite ) - usethis::use_build_ignore("src/.cargo") - edition <- match.arg(edition, several.ok = FALSE) cargo_toml_content <- to_toml( package = list(name = crate_name, publish = FALSE, version = "0.1.0", edition = edition), @@ -172,6 +170,61 @@ use_extendr <- function(path = ".", data = list(pkg_name = pkg_name) ) + # create tools directory if it does not exist + if (!dir.exists("tools")) { + dir.create("tools") + } + + # add msrv.R template + use_rextendr_template( + "msrv.R", + save_as = file.path("tools", "msrv.R"), + quiet = quiet, + overwrite = overwrite + ) + + # add configure and configure.win templates + use_rextendr_template( + "configure", + save_as = "configure", + quiet = quiet, + overwrite = overwrite, + data = list(lib_name = lib_name) + ) + + use_rextendr_template( + "configure.win", + save_as = "configure.win", + quiet = quiet, + overwrite = overwrite, + data = list(lib_name = lib_name) + ) + + # configure needs to be made executable + # ignore for Windows + if (.Platform[["OS.type"]] == "unix") { + Sys.chmod("configure", "0755") + } + + # the temporary cargo directory must be ignored + usethis::use_build_ignore("src/.cargo") + + # ensure that the vendor directory is ignored + usethis::use_build_ignore( + file.path("src", "rust", "vendor") + ) + + usethis::use_git_ignore( + file.path("src", "rust", "vendor") + ) + + # the src/Makevars should be created each time the package + # is built. This is handled via the configure file + usethis::use_build_ignore("src/Makevars") + usethis::use_git_ignore("src/Makevars") + usethis::use_build_ignore("src/Makevars.win") + usethis::use_git_ignore("src/Makevars.win") + if (!isTRUE(quiet)) { cli::cli_alert_success("Finished configuring {.pkg extendr} for package {.pkg {pkg_name}}.") cli::cli_ul( diff --git a/R/use_msrv.R b/R/use_msrv.R index 340998fa..7133642f 100644 --- a/R/use_msrv.R +++ b/R/use_msrv.R @@ -52,14 +52,16 @@ use_msrv <- function(version, path = ".", overwrite = FALSE) { ) }) - desc_path <- rprojroot::find_package_root_file("DESCRIPTION", path = path) - - if (!file.exists(desc_path)) { - cli::cli_abort( - "{.arg path} ({.path {path}}) does not contain a DESCRIPTION", - class = "rextendr_error" - ) - } + desc_path <- rlang::try_fetch( + rprojroot::find_package_root_file("DESCRIPTION", path = path), + error = function(cnd) { + cli::cli_abort( + "{.arg path} ({.path {path}}) does not contain a DESCRIPTION", + class = "rextendr_error", + call = rlang::env_parent() + ) + } + ) cur <- paste("Cargo (Rust's package manager), rustc", paste(">=", version)) diff --git a/R/utils.R b/R/utils.R index 9717ea49..d408c454 100644 --- a/R/utils.R +++ b/R/utils.R @@ -49,7 +49,7 @@ cargo_command_available <- function(args = "--help") { try_exec_cmd <- function(cmd, args = character()) { result <- tryCatch( processx::run(cmd, args, error_on_status = FALSE), - error = \(...) list(status = -1) + error = function(...) list(status = -1) ) if (result[["status"]] != 0) { NA_character_ @@ -57,3 +57,23 @@ try_exec_cmd <- function(cmd, args = character()) { stringi::stri_split_lines1(result$stdout) } } + +#' Replace missing values in vector +#' +#' @param data vector, data with missing values to replace +#' @param replace scalar, value to substitute for missing values in data +#' @param ... currently ignored +#' +#' @keywords internal +#' @noRd +#' +replace_na <- function(data, replace = NA, ...) { + if (vctrs::vec_any_missing(data)) { + missing <- vctrs::vec_detect_missing(data) + data <- vctrs::vec_assign(data, missing, replace, + x_arg = "data", + value_arg = "replace" + ) + } + data +} diff --git a/README.Rmd b/README.Rmd index 56be3c57..0bc04b4a 100644 --- a/README.Rmd +++ b/README.Rmd @@ -21,7 +21,7 @@ knitr::opts_chunk$set( [![rextendr status badge](https://extendr.r-universe.dev/badges/rextendr)](https://extendr.r-universe.dev/rextendr) [![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) [![R build status](https://github.com/extendr/rextendr/workflows/R-CMD-check/badge.svg)](https://github.com/extendr/rextendr/actions) -[![codecov](https://codecov.io/gh/extendr/rextendr/branch/main/graph/badge.svg?token=5H6ID0LAO7)](https://app.codecov.io/gh/extendr/rextendr) +[![Codecov test coverage](https://codecov.io/gh/extendr/rextendr/graph/badge.svg)](https://app.codecov.io/gh/extendr/rextendr) ## Installation diff --git a/_pkgdown.yml b/_pkgdown.yml index eddfcfa1..56281954 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -22,6 +22,7 @@ reference: - write_license_note - clean - cran + - vendor_pkgs - use_msrv - title: Various utility functions @@ -29,3 +30,4 @@ reference: - to_toml - make_module_macro - rust_sitrep + - read_cargo_metadata diff --git a/codecov.yml b/codecov.yml index 04c55859..260850ef 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,14 +1,14 @@ -comment: false - +comment: + layout: "header, reach, files" + require_changes: true + behavior: "new" # Only post on new commits coverage: status: project: default: - target: auto - threshold: 1% - informational: true + target: 70% + threshold: 5% patch: default: - target: auto - threshold: 1% - informational: true + target: 70% + threshold: 5% diff --git a/inst/templates/Makevars b/inst/templates/Makevars deleted file mode 100644 index 07ed90f4..00000000 --- a/inst/templates/Makevars +++ /dev/null @@ -1,30 +0,0 @@ -TARGET_DIR = ./rust/target -LIBDIR = $(TARGET_DIR)/release -STATLIB = $(LIBDIR)/lib{{{lib_name}}}.a -PKG_LIBS = -L$(LIBDIR) -l{{{lib_name}}} - -all: C_clean - -$(SHLIB): $(STATLIB) - -CARGOTMP = $(CURDIR)/.cargo - -$(STATLIB): - # In some environments, ~/.cargo/bin might not be included in PATH, so we need - # to set it here to ensure cargo can be invoked. It is appended to PATH and - # therefore is only used if cargo is absent from the user's PATH. - if [ "$(NOT_CRAN)" != "true" ]; then \ - export CARGO_HOME=$(CARGOTMP); \ - fi && \ - export PATH="$(PATH):$(HOME)/.cargo/bin" && \ - cargo build --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) - if [ "$(NOT_CRAN)" != "true" ]; then \ - rm -Rf $(CARGOTMP) && \ - rm -Rf $(LIBDIR)/build; \ - fi - -C_clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) - -clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) diff --git a/inst/templates/Makevars.in b/inst/templates/Makevars.in new file mode 100644 index 00000000..1f947ad7 --- /dev/null +++ b/inst/templates/Makevars.in @@ -0,0 +1,44 @@ +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/release +STATLIB = $(LIBDIR)/lib{{{lib_name}}}.a +PKG_LIBS = -L$(LIBDIR) -l{{{lib_name}}} + +all: C_clean + +$(SHLIB): $(STATLIB) + +CARGOTMP = $(CURDIR)/.cargo +VENDOR_DIR = $(CURDIR)/vendor + + +# RUSTFLAGS appends --print=native-static-libs to ensure that +# the correct linkers are used. Use this for debugging if need. +# +# CRAN note: Cargo and Rustc versions are reported during +# configure via tools/msrv.R. +# +# When the NOT_CRAN flag is *not* set, the vendor.tar.xz, if present, +# is unzipped and used for offline compilation. +$(STATLIB): + + # Check if NOT_CRAN is false and unzip vendor.tar.xz if so + if [ "$(NOT_CRAN)" != "true" ]; then \ + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi; \ + fi + + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + +C_clean: + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) + +clean: + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) diff --git a/inst/templates/Makevars.win b/inst/templates/Makevars.win.in similarity index 60% rename from inst/templates/Makevars.win rename to inst/templates/Makevars.win.in index e47a9ab2..78e0bbfd 100644 --- a/inst/templates/Makevars.win +++ b/inst/templates/Makevars.win.in @@ -21,18 +21,26 @@ $(STATLIB): # https://github.com/r-windows/rtools-packages/blob/2407b23f1e0925bbb20a4162c963600105236318/mingw-w64-gcc/PKGBUILD#L313-L316 touch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a - # CARGO_LINKER is provided in Makevars.ucrt for R >= 4.2 - if [ "$(NOT_CRAN)" != "true" ]; then \ - export CARGO_HOME=$(CARGOTMP); \ - fi && \ - export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \ - export LIBRARY_PATH="$${LIBRARY_PATH};$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ - cargo build --target=$(TARGET) --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + # When the NOT_CRAN flag is *not* set, the vendor.tar.xz, if present, + # is unzipped and used for offline compilation. if [ "$(NOT_CRAN)" != "true" ]; then \ - rm -Rf $(CARGOTMP) && \ - rm -Rf $(LIBDIR)/build; \ + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi; \ fi + # CARGO_LINKER is provided in Makevars.ucrt for R >= 4.2 + # Build the project using Cargo with additional flags + export CARGO_HOME=$(CARGOTMP) && \ + export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \ + export LIBRARY_PATH="$${LIBRARY_PATH};$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --target=$(TARGET) --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + C_clean: rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) diff --git a/inst/templates/configure b/inst/templates/configure new file mode 100644 index 00000000..5c1355a1 --- /dev/null +++ b/inst/templates/configure @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +: "${R_HOME=`R RHOME`}" +"${R_HOME}/bin/Rscript" tools/msrv.R + +# Set CRAN_FLAGS based on the NOT_CRAN value +if [ "${NOT_CRAN}" != "true" ] && [ -f ./src/rust/vendor.tar.xz ]; then + export CRAN_FLAGS="-j 2 --offline" +else + export CRAN_FLAGS="" +fi + +# delete Makevars if it is present +[ -f src/Makevars ] && rm src/Makevars + +# Substitute @CRAN_FLAGS@ in Makevars.in with the actual value of $CRAN_FLAGS +sed -e "s|@CRAN_FLAGS@|$CRAN_FLAGS|" src/Makevars.in > src/Makevars \ No newline at end of file diff --git a/inst/templates/configure.win b/inst/templates/configure.win new file mode 100644 index 00000000..f6d1efbd --- /dev/null +++ b/inst/templates/configure.win @@ -0,0 +1,15 @@ +#!/usr/bin/env sh +"${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/msrv.R + +# Set CRAN_FLAGS based on the NOT_CRAN value +if [ "${NOT_CRAN}" != "true" ] && [ -f ./src/rust/vendor.tar.xz ]; then + export CRAN_FLAGS="-j 2 --offline" +else + export CRAN_FLAGS="" +fi + +# delete Makevars.win if it is present +[ -f src/Makevars.win ] && rm src/Makevars.win + +# Substitute @CRAN_FLAGS@ in Makevars.in with the actual value of $CRAN_FLAGS +sed -e "s|@CRAN_FLAGS@|$CRAN_FLAGS|" src/Makevars.win.in > src/Makevars.win \ No newline at end of file diff --git a/inst/templates/cran/Makevars b/inst/templates/cran/Makevars deleted file mode 100644 index e2dc513e..00000000 --- a/inst/templates/cran/Makevars +++ /dev/null @@ -1,36 +0,0 @@ -TARGET_DIR = ./rust/target -LIBDIR = $(TARGET_DIR)/release -STATLIB = $(LIBDIR)/lib{{{lib_name}}}.a -PKG_LIBS = -L$(LIBDIR) -l{{{lib_name}}} - -all: C_clean - -$(SHLIB): $(STATLIB) - -CRAN_FLAGS=-j 2 --offline -CARGOTMP = $(CURDIR)/.cargo -VENDOR_DIR = $(CURDIR)/vendor - -$(STATLIB): - if [ -f ./rust/vendor.tar.xz ]; then \ - tar xf rust/vendor.tar.xz && \ - mkdir -p $(CARGOTMP) && \ - cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ - fi - - # In some environments, ~/.cargo/bin might not be included in PATH, so we need - # to set it here to ensure cargo can be invoked. It is appended to PATH and - # therefore is only used if cargo is absent from the user's PATH. - if [ "$(NOT_CRAN)" != "true" ]; then \ - export CARGO_HOME=$(CARGOTMP); \ - fi && \ - export PATH="$(PATH):$(HOME)/.cargo/bin" && \ - cargo build $(CRAN_FLAGS) --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) && \ - echo `cargo --version` && echo `rustc --version`; - rm -Rf $(CARGOTMP) $(VENDOR_DIR) $(LIBDIR)/build; \ - -C_clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(CARGOTMP) $(VENDOR_DIR) - -clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(CARGOTMP) $(VENDOR_DIR) $(TARGET_DIR) diff --git a/inst/templates/cran/Makevars.win b/inst/templates/cran/Makevars.win deleted file mode 100644 index f19bb836..00000000 --- a/inst/templates/cran/Makevars.win +++ /dev/null @@ -1,48 +0,0 @@ -TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu - -TARGET_DIR = ./rust/target -LIBDIR = $(TARGET_DIR)/$(TARGET)/release -STATLIB = $(LIBDIR)/lib{{{lib_name}}}.a -PKG_LIBS = -L$(LIBDIR) -l{{{lib_name}}} -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll - -all: C_clean - -$(SHLIB): $(STATLIB) - -CRAN_FLAGS=-j 2 --offline -CARGOTMP = $(CURDIR)/.cargo -VENDOR_DIR = $(CURDIR)/vendor - -$(STATLIB): - # uncompress vendored deps - if [ -f ./rust/vendor.tar.xz ]; then \ - tar xf rust/vendor.tar.xz && \ - mkdir -p $(CARGOTMP) && \ - cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ - fi - - mkdir -p $(TARGET_DIR)/libgcc_mock - # `rustc` adds `-lgcc_eh` flags to the compiler, but Rtools' GCC doesn't have - # `libgcc_eh` due to the compilation settings. So, in order to please the - # compiler, we need to add empty `libgcc_eh` to the library search paths. - # For more details, please refer to - # https://github.com/r-windows/rtools-packages/blob/2407b23f1e0925bbb20a4162c963600105236318/mingw-w64-gcc/PKGBUILD#L313-L316 - touch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a - - # CARGO_LINKER is provided in Makevars.ucrt for R >= 4.2 - if [ "$(NOT_CRAN)" != "true" ]; then \ - export CARGO_HOME=$(CARGOTMP); \ - fi && \ - export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \ - export LIBRARY_PATH="$${LIBRARY_PATH};$(CURDIR)/$(TARGET_DIR)/libgcc_mock"; \ - cargo build $(CRAN_FLAGS) --target=$(TARGET) --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) && \ - echo `cargo --version` && echo `rustc --version`; - if [ "$(NOT_CRAN)" != "true" ]; then \ - rm -Rf $(CARGOTMP) $(VENDOR_DIR) $(LIBDIR)/build; \ - fi - -C_clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(CARGOTMP) $(VENDOR_DIR) - -clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(CARGOTMP) $(VENDOR_DIR) $(TARGET_DIR) diff --git a/inst/templates/cran/configure b/inst/templates/cran/configure deleted file mode 100644 index 0f4c1be0..00000000 --- a/inst/templates/cran/configure +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env sh -: "${R_HOME=`R RHOME`}" -"${R_HOME}/bin/Rscript" tools/msrv.R diff --git a/inst/templates/cran/configure.win b/inst/templates/cran/configure.win deleted file mode 100644 index f1945ac1..00000000 --- a/inst/templates/cran/configure.win +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -"${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/msrv.R diff --git a/inst/templates/cran/msrv.R b/inst/templates/msrv.R similarity index 100% rename from inst/templates/cran/msrv.R rename to inst/templates/msrv.R diff --git a/man/clean.Rd b/man/clean.Rd index 9f0646e4..6a756a9b 100644 --- a/man/clean.Rd +++ b/man/clean.Rd @@ -4,10 +4,16 @@ \alias{clean} \title{Clean Rust binaries and package cache.} \usage{ -clean(path = ".") +clean(path = ".", echo = TRUE) } \arguments{ -\item{path}{[ string ] Path to the package root.} +\item{path}{character scalar, path to R package root.} + +\item{echo}{logical scalar, should cargo command and outputs be printed to +console (default is \code{TRUE})} +} +\value{ +character vector with names of all deleted files (invisibly). } \description{ Removes Rust binaries (such as \code{.dll}/\code{.so} libraries), C wrapper object files, @@ -15,3 +21,8 @@ invokes \verb{cargo clean} to reset cargo target directory (found by default at \verb{pkg_root/src/rust/target/}). Useful when Rust code should be recompiled from scratch. } +\examples{ +\dontrun{ +clean() +} +} diff --git a/man/cran.Rd b/man/cran.Rd index 92fb04d9..05a481fe 100644 --- a/man/cran.Rd +++ b/man/cran.Rd @@ -2,52 +2,34 @@ % Please edit documentation in R/cran-compliance.R \name{cran} \alias{cran} -\alias{use_cran_defaults} -\alias{vendor_pkgs} -\title{Use CRAN compliant defaults} -\usage{ -use_cran_defaults(path = ".", quiet = FALSE, overwrite = NULL, lib_name = NULL) - -vendor_pkgs(path = ".", quiet = FALSE, overwrite = NULL) +\title{CRAN compliant extendr packages} +\description{ +R packages developed using extendr are not immediately ready to +be published to CRAN. The extendr package template ensures that +CRAN publication is (farily) painless. } -\arguments{ -\item{path}{File path to the package for which to generate wrapper code.} - -\item{quiet}{Logical indicating whether any progress messages should be -generated or not.} +\section{CRAN requirements}{ -\item{overwrite}{Logical scalar or \code{NULL} indicating whether the files in the \code{path} should be overwritten. -If \code{NULL} (default), the function will ask the user whether each file should -be overwritten in an interactive session or do nothing in a non-interactive session. -If \code{FALSE} and each file already exists, the function will do nothing. -If \code{TRUE}, all files will be overwritten.} -\item{lib_name}{String that is used as the name of the Rust library. -If \code{NULL}, sanitized R package name is used instead.} -} -\value{ +In order to publish a Rust based package on CRAN it must meet certain +requirements. These are: \itemize{ -\item \code{vendor_pkgs()} returns a data.frame with two columns \code{crate} and \code{version} -\item \code{use_cran_defaults()} returns \code{NULL} and is used solely for its side effects -} +\item Rust dependencies are vendored +\item The package is compiled offline +\item the \code{DESCRIPTION} file's \code{SystemRequirements} field contains \verb{Cargo (Rust's package manager), rustc} } -\description{ -Modifies an extendr package to use CRAN compliant settings. -} -\details{ -\code{use_cran_defaults()} modifies an existing package to provide CRAN complaint -settings and files. It creates \code{tools/msrv.R}, \code{configure} and \code{configure.win} files as well as -modifies \code{Makevars} and \code{Makevars.win} to use required CRAN settings. -\code{vendor_pkgs()} is used to package the dependencies as required by CRAN. -It executes \verb{cargo vendor} on your behalf creating a \verb{vendor/} directory and a -compressed \code{vendor.tar.xz} which will be shipped with package itself. -If you have modified your dependencies, you will need need to repackage -} -\examples{ +The extendr templates handle all of this \emph{except} vendoring dependencies. +This must be done prior to publication using \code{\link[=vendor_pkgs]{vendor_pkgs()}}. -if (interactive()) { - use_cran_defaults() - vendor_pkgs() -} +In addition, it is important to make sure that CRAN maintainers +are aware that the package they are checking contains Rust code. +Depending on which and how many crates are used as a dependencies +the \code{vendor.tar.xz} will be larger than a few megabytes. If a +built package is larger than 5mbs CRAN may reject the submission. + +To prevent rejection make a note in your \code{cran-comments.md} file +(create one using \code{\link[usethis:use_cran_comments]{usethis::use_cran_comments()}}) along the lines of +"The package tarball is 6mb because Rust dependencies are vendored within src/rust/vendor.tar.xz which is 5.9mb." } + diff --git a/man/read_cargo_metadata.Rd b/man/read_cargo_metadata.Rd new file mode 100644 index 00000000..16aea079 --- /dev/null +++ b/man/read_cargo_metadata.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/read_cargo_metadata.R +\name{read_cargo_metadata} +\alias{read_cargo_metadata} +\title{Retrieve metadata for packages and workspaces} +\usage{ +read_cargo_metadata(path = ".", dependencies = FALSE, echo = FALSE) +} +\arguments{ +\item{path}{character scalar, the R package directory} + +\item{dependencies}{Default \code{FALSE}. A logical scalar, whether to include +all recursive dependencies in stdout.} + +\item{echo}{Default \code{FALSE}. A logical scalar, should cargo command and +outputs be printed to the console.} +} +\value{ +A \code{list} including the following elements: +\itemize{ +\item \code{packages} +\item \code{workspace_members} +\item \code{workspace_default_members} +\item \code{resolve} +\item \code{target_directory} +\item \code{version} +\item \code{workspace_root} +\item \code{metadata} +} +} +\description{ +Retrieve metadata for packages and workspaces +} +\details{ +For more details, see +\href{https://doc.rust-lang.org/cargo/commands/cargo-metadata.html}{Cargo docs} +for \code{cargo-metadata}. See especially "JSON Format" to get a sense of what you +can expect to find in the returned list. +} +\examples{ +\dontrun{ +read_cargo_metadata() +} + +} diff --git a/man/use_crate.Rd b/man/use_crate.Rd index 1500d6bd..2341681f 100644 --- a/man/use_crate.Rd +++ b/man/use_crate.Rd @@ -10,7 +10,8 @@ use_crate( git = NULL, version = NULL, optional = FALSE, - path = "." + path = ".", + echo = TRUE ) } \arguments{ @@ -27,9 +28,12 @@ crate} (FALSE by default)} \item{path}{character scalar, the package directory} + +\item{echo}{logical scalar, should cargo command and outputs be printed to +console (default is TRUE)} } \value{ -\code{NULL}, invisibly +\code{NULL} (invisibly) } \description{ Analogous to \code{usethis::use_package()} but for crate dependencies. diff --git a/man/vendor_pkgs.Rd b/man/vendor_pkgs.Rd new file mode 100644 index 00000000..8e133c19 --- /dev/null +++ b/man/vendor_pkgs.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cran-compliance.R +\name{vendor_pkgs} +\alias{vendor_pkgs} +\title{Vendor Rust dependencies} +\usage{ +vendor_pkgs(path = ".", quiet = FALSE, overwrite = NULL) +} +\arguments{ +\item{path}{File path to the package for which to generate wrapper code.} + +\item{quiet}{Logical indicating whether any progress messages should be +generated or not.} + +\item{overwrite}{Logical scalar or \code{NULL} indicating whether the files in the \code{path} should be overwritten. +If \code{NULL} (default), the function will ask the user whether each file should +be overwritten in an interactive session or do nothing in a non-interactive session. +If \code{FALSE} and each file already exists, the function will do nothing. +If \code{TRUE}, all files will be overwritten.} +} +\value{ +\itemize{ +\item \code{vendor_pkgs()} returns a data.frame with two columns \code{crate} and \code{version} +} +} +\description{ +\code{vendor_pkgs()} is used to package the dependencies as required by CRAN. +It executes \verb{cargo vendor} on your behalf creating a \verb{vendor/} directory and a +compressed \code{vendor.tar.xz} which will be shipped with package itself. +If you have modified your dependencies, you will need need to repackage +} +\examples{ +\dontrun{ +vendor_pkgs() +} +} diff --git a/man/write_license_note.Rd b/man/write_license_note.Rd index cfdfb835..6bf93d53 100644 --- a/man/write_license_note.Rd +++ b/man/write_license_note.Rd @@ -7,17 +7,23 @@ write_license_note(path = ".", quiet = FALSE, force = TRUE) } \arguments{ -\item{path}{Path from which package root is looked up.} +\item{path}{character scalar, the R package directory} -\item{quiet}{Logical indicating whether any progress messages should be -generated or not.} +\item{quiet}{logical scalar, whether to signal successful writing of +LICENSE.note (default is \code{FALSE})} -\item{force}{Logical indicating whether to regenerate LICENSE.note if LICENSE.note already exists.} +\item{force}{logical scalar, whether to regenerate LICENSE.note if +LICENSE.note already exists (default is \code{TRUE})} } \value{ -No return value, called for side effects. +text printed to LICENSE.note (invisibly). } \description{ -LICENSE.note generated by this function contains information about Rust crate dependencies. -To use this function, the \href{https://crates.io/crates/cargo-license}{cargo-license} command must be installed. +LICENSE.note generated by this function contains information about all +recursive dependencies in Rust crate. +} +\examples{ +\dontrun{ +write_license_note() +} } diff --git a/tests/testthat/_snaps/cran-compliance.md b/tests/testthat/_snaps/cran-compliance.md new file mode 100644 index 00000000..e7dfdfa0 --- /dev/null +++ b/tests/testthat/_snaps/cran-compliance.md @@ -0,0 +1,29 @@ +# vendor_pkgs() vendors dependencies + + Code + cat_file("src", "rust", "vendor-config.toml") + Output + [source.crates-io] + replace-with = "vendored-sources" + + [source.vendored-sources] + directory = "vendor" + +--- + + Code + package_versions + Output + # A tibble: 9 x 2 + crate version + + 1 extendr-api *.*.* + 2 extendr-macros *.*.* + 3 libR-sys *.*.* + 4 once_cell *.*.* + 5 paste *.*.* + 6 proc-macro2 *.*.* + 7 quote *.*.* + 8 syn *.*.* + 9 unicode-ident *.*.* + diff --git a/tests/testthat/_snaps/license_note.md b/tests/testthat/_snaps/license_note.md index d2757ccb..44be7cc5 100644 --- a/tests/testthat/_snaps/license_note.md +++ b/tests/testthat/_snaps/license_note.md @@ -32,42 +32,42 @@ Name: once_cell Repository: https://github.com/matklad/once_cell Authors: Aleksey Kladov - License: Apache-2.0 OR MIT + License: MIT OR Apache-2.0 ------------------------------------------------------------- Name: paste Repository: https://github.com/dtolnay/paste Authors: David Tolnay - License: Apache-2.0 OR MIT + License: MIT OR Apache-2.0 ------------------------------------------------------------- Name: proc-macro2 Repository: https://github.com/dtolnay/proc-macro2 Authors: David Tolnay, Alex Crichton - License: Apache-2.0 OR MIT + License: MIT OR Apache-2.0 ------------------------------------------------------------- Name: quote Repository: https://github.com/dtolnay/quote Authors: David Tolnay - License: Apache-2.0 OR MIT + License: MIT OR Apache-2.0 ------------------------------------------------------------- Name: syn Repository: https://github.com/dtolnay/syn Authors: David Tolnay - License: Apache-2.0 OR MIT + License: MIT OR Apache-2.0 ------------------------------------------------------------- Name: unicode-ident Repository: https://github.com/dtolnay/unicode-ident Authors: David Tolnay - License: (MIT OR Apache-2.0) AND Unicode-DFS-2016 + License: (MIT OR Apache-2.0) AND Unicode-3.0 ------------------------------------------------------------- diff --git a/tests/testthat/_snaps/use_cran_defaults.md b/tests/testthat/_snaps/use_cran_defaults.md deleted file mode 100644 index 49ef14b8..00000000 --- a/tests/testthat/_snaps/use_cran_defaults.md +++ /dev/null @@ -1,291 +0,0 @@ -# use_cran_defaults() modifies and creates files correctly - - Code - use_extendr() - Message - i First time using rextendr. Upgrading automatically... - i Setting `Config/rextendr/version` to "0.3.1.9001" in the 'DESCRIPTION' file. - i Setting `SystemRequirements` to "Cargo (Rust's package manager), rustc" in the 'DESCRIPTION' file. - v Creating 'src/rust/src'. - v Writing 'src/entrypoint.c' - v Writing 'src/Makevars' - v Writing 'src/Makevars.win' - v Writing 'src/Makevars.ucrt' - v Writing 'src/.gitignore' - v Adding "^src/\\.cargo$" to '.Rbuildignore'. - v Writing 'src/rust/Cargo.toml' - v Writing 'src/rust/src/lib.rs' - v Writing 'src/testpkg-win.def' - v Writing 'R/extendr-wrappers.R' - v Finished configuring extendr for package testpkg. - * Please run `rextendr::document()` for changes to take effect. - ---- - - Code - use_cran_defaults() - Message - v Writing 'tools/msrv.R' - v Writing 'configure' - v Writing 'configure.win' - > File 'src/Makevars' already exists. Skip writing the file. - > File 'src/Makevars.win' already exists. Skip writing the file. - v Adding "^src/rust/vendor$" to '.Rbuildignore'. - v Adding "src/rust/vendor" to '.gitignore'. - ---- - - Code - cat_file("src", "Makevars") - Output - TARGET_DIR = ./rust/target - LIBDIR = $(TARGET_DIR)/release - STATLIB = $(LIBDIR)/libtestpkg.a - PKG_LIBS = -L$(LIBDIR) -ltestpkg - - all: C_clean - - $(SHLIB): $(STATLIB) - - CARGOTMP = $(CURDIR)/.cargo - - $(STATLIB): - # In some environments, ~/.cargo/bin might not be included in PATH, so we need - # to set it here to ensure cargo can be invoked. It is appended to PATH and - # therefore is only used if cargo is absent from the user's PATH. - if [ "$(NOT_CRAN)" != "true" ]; then \ - export CARGO_HOME=$(CARGOTMP); \ - fi && \ - export PATH="$(PATH):$(HOME)/.cargo/bin" && \ - cargo build --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) - if [ "$(NOT_CRAN)" != "true" ]; then \ - rm -Rf $(CARGOTMP) && \ - rm -Rf $(LIBDIR)/build; \ - fi - - C_clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) - - clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) - ---- - - Code - cat_file("src", "Makevars.win") - Output - TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu - - TARGET_DIR = ./rust/target - LIBDIR = $(TARGET_DIR)/$(TARGET)/release - STATLIB = $(LIBDIR)/libtestpkg.a - PKG_LIBS = -L$(LIBDIR) -ltestpkg -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll - - all: C_clean - - $(SHLIB): $(STATLIB) - - CARGOTMP = $(CURDIR)/.cargo - - $(STATLIB): - mkdir -p $(TARGET_DIR)/libgcc_mock - # `rustc` adds `-lgcc_eh` flags to the compiler, but Rtools' GCC doesn't have - # `libgcc_eh` due to the compilation settings. So, in order to please the - # compiler, we need to add empty `libgcc_eh` to the library search paths. - # - # For more details, please refer to - # https://github.com/r-windows/rtools-packages/blob/2407b23f1e0925bbb20a4162c963600105236318/mingw-w64-gcc/PKGBUILD#L313-L316 - touch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a - - # CARGO_LINKER is provided in Makevars.ucrt for R >= 4.2 - if [ "$(NOT_CRAN)" != "true" ]; then \ - export CARGO_HOME=$(CARGOTMP); \ - fi && \ - export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \ - export LIBRARY_PATH="$${LIBRARY_PATH};$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ - cargo build --target=$(TARGET) --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) - if [ "$(NOT_CRAN)" != "true" ]; then \ - rm -Rf $(CARGOTMP) && \ - rm -Rf $(LIBDIR)/build; \ - fi - - C_clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) - - clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) - ---- - - Code - cat_file("configure") - Output - #!/usr/bin/env sh - : "${R_HOME=`R RHOME`}" - "${R_HOME}/bin/Rscript" tools/msrv.R - ---- - - Code - cat_file("configure.win") - Output - #!/usr/bin/env sh - "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/msrv.R - ---- - - Code - cat_file("tools", "msrv.R") - Output - # read the DESCRIPTION file - desc <- read.dcf("DESCRIPTION") - - if (!"SystemRequirements" %in% colnames(desc)) { - fmt <- c( - "`SystemRequirements` not found in `DESCRIPTION`.", - "Please specify `SystemRequirements: Cargo (Rust's package manager), rustc`" - ) - stop(paste(fmt, collapse = "\n")) - } - - # extract system requirements - sysreqs <- desc[, "SystemRequirements"] - - # check that cargo and rustc is found - if (!grepl("cargo", sysreqs, ignore.case = TRUE)) { - stop("You must specify `Cargo (Rust's package manager)` in your `SystemRequirements`") - } - - if (!grepl("rustc", sysreqs, ignore.case = TRUE)) { - stop("You must specify `Cargo (Rust's package manager), rustc` in your `SystemRequirements`") - } - - # split into parts - parts <- strsplit(sysreqs, ", ")[[1]] - - # identify which is the rustc - rustc_ver <- parts[grepl("rustc", parts)] - - # perform checks for the presence of rustc and cargo on the OS - no_cargo_msg <- c( - "----------------------- [CARGO NOT FOUND]--------------------------", - "The 'cargo' command was not found on the PATH. Please install Cargo", - "from: https://www.rust-lang.org/tools/install", - "", - "Alternatively, you may install Cargo from your OS package manager:", - " - Debian/Ubuntu: apt-get install cargo", - " - Fedora/CentOS: dnf install cargo", - " - macOS: brew install rustc", - "-------------------------------------------------------------------" - ) - - no_rustc_msg <- c( - "----------------------- [RUST NOT FOUND]---------------------------", - "The 'rustc' compiler was not found on the PATH. Please install", - paste(rustc_ver, "or higher from:"), - "https://www.rust-lang.org/tools/install", - "", - "Alternatively, you may install Rust from your OS package manager:", - " - Debian/Ubuntu: apt-get install rustc", - " - Fedora/CentOS: dnf install rustc", - " - macOS: brew install rustc", - "-------------------------------------------------------------------" - ) - - # Add {user}/.cargo/bin to path before checking - new_path <- paste0( - Sys.getenv("PATH"), - ":", - paste0(Sys.getenv("HOME"), "/.cargo/bin") - ) - - # set the path with the new path - Sys.setenv("PATH" = new_path) - - # check for rustc installation - rustc_version <- tryCatch( - system("rustc --version", intern = TRUE), - error = function(e) { - stop(paste(no_rustc_msg, collapse = "\n")) - } - ) - - # check for cargo installation - cargo_version <- tryCatch( - system("cargo --version", intern = TRUE), - error = function(e) { - stop(paste(no_cargo_msg, collapse = "\n")) - } - ) - - # helper function to extract versions - extract_semver <- function(ver) { - if (grepl("\\d+\\.\\d+(\\.\\d+)?", ver)) { - sub(".*?(\\d+\\.\\d+(\\.\\d+)?).*", "\\1", ver) - } else { - NA - } - } - - # get the MSRV - msrv <- extract_semver(rustc_ver) - - # extract current version - current_rust_version <- extract_semver(rustc_version) - - # perform check - if (!is.na(msrv)) { - # -1 when current version is later - # 0 when they are the same - # 1 when MSRV is newer than current - is_msrv <- utils::compareVersion(msrv, current_rust_version) - if (is_msrv == 1) { - fmt <- paste0( - "\n------------------ [UNSUPPORTED RUST VERSION]------------------\n", - "- Minimum supported Rust version is %s.\n", - "- Installed Rust version is %s.\n", - "---------------------------------------------------------------" - ) - stop(sprintf(fmt, msrv, current_rust_version)) - } - } - - # print the versions - versions_fmt <- "Using %s\nUsing %s" - message(sprintf(versions_fmt, cargo_version, rustc_version)) - -# use_cran_defaults() quiet if quiet=TRUE - - Code - use_extendr(quiet = TRUE) - use_cran_defaults(quiet = TRUE) - -# vendor_pkgs() vendors dependencies - - Code - cat_file("src", "rust", "vendor-config.toml") - Output - [source.crates-io] - replace-with = "vendored-sources" - - [source.vendored-sources] - directory = "vendor" - ---- - - Code - package_versions - Output - # A tibble: 9 x 2 - crate version - - 1 extendr-api *.*.* - 2 extendr-macros *.*.* - 3 libR-sys *.*.* - 4 once_cell *.*.* - 5 paste *.*.* - 6 proc-macro2 *.*.* - 7 quote *.*.* - 8 syn *.*.* - 9 unicode-ident *.*.* - diff --git a/tests/testthat/_snaps/use_extendr.md b/tests/testthat/_snaps/use_extendr.md index 28141539..6d53afcf 100644 --- a/tests/testthat/_snaps/use_extendr.md +++ b/tests/testthat/_snaps/use_extendr.md @@ -1,24 +1,186 @@ # use_extendr() sets up extendr files correctly Code - use_extendr() - Message - i First time using rextendr. Upgrading automatically... - i Setting `Config/rextendr/version` to "0.3.1.9001" in the 'DESCRIPTION' file. - i Setting `SystemRequirements` to "Cargo (Rust's package manager), rustc" in the 'DESCRIPTION' file. - v Creating 'src/rust/src'. - v Writing 'src/entrypoint.c' - v Writing 'src/Makevars' - v Writing 'src/Makevars.win' - v Writing 'src/Makevars.ucrt' - v Writing 'src/.gitignore' - v Adding "^src/\\.cargo$" to '.Rbuildignore'. - v Writing 'src/rust/Cargo.toml' - v Writing 'src/rust/src/lib.rs' - v Writing 'src/testpkg-win.def' - v Writing 'R/extendr-wrappers.R' - v Finished configuring extendr for package testpkg. - * Please run `rextendr::document()` for changes to take effect. + cat_file(".gitignore") + Output + src/rust/vendor + src/Makevars + src/Makevars.win + +--- + + Code + cat_file(".Rbuildignore") + Output + ^src/\.cargo$ + ^src/rust/vendor$ + ^src/Makevars$ + ^src/Makevars\.win$ + +--- + + Code + cat_file("configure") + Output + #!/usr/bin/env sh + : "${R_HOME=`R RHOME`}" + "${R_HOME}/bin/Rscript" tools/msrv.R + + # Set CRAN_FLAGS based on the NOT_CRAN value + if [ "${NOT_CRAN}" != "true" ] && [ -f ./src/rust/vendor.tar.xz ]; then + export CRAN_FLAGS="-j 2 --offline" + else + export CRAN_FLAGS="" + fi + + # delete Makevars if it is present + [ -f src/Makevars ] && rm src/Makevars + + # Substitute @CRAN_FLAGS@ in Makevars.in with the actual value of $CRAN_FLAGS + sed -e "s|@CRAN_FLAGS@|$CRAN_FLAGS|" src/Makevars.in > src/Makevars + +--- + + Code + cat_file("configure.win") + Output + #!/usr/bin/env sh + "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/msrv.R + + # Set CRAN_FLAGS based on the NOT_CRAN value + if [ "${NOT_CRAN}" != "true" ] && [ -f ./src/rust/vendor.tar.xz ]; then + export CRAN_FLAGS="-j 2 --offline" + else + export CRAN_FLAGS="" + fi + + # delete Makevars.win if it is present + [ -f src/Makevars.win ] && rm src/Makevars.win + + # Substitute @CRAN_FLAGS@ in Makevars.in with the actual value of $CRAN_FLAGS + sed -e "s|@CRAN_FLAGS@|$CRAN_FLAGS|" src/Makevars.win.in > src/Makevars.win + +--- + + Code + cat_file("tools", "msrv.R") + Output + # read the DESCRIPTION file + desc <- read.dcf("DESCRIPTION") + + if (!"SystemRequirements" %in% colnames(desc)) { + fmt <- c( + "`SystemRequirements` not found in `DESCRIPTION`.", + "Please specify `SystemRequirements: Cargo (Rust's package manager), rustc`" + ) + stop(paste(fmt, collapse = "\n")) + } + + # extract system requirements + sysreqs <- desc[, "SystemRequirements"] + + # check that cargo and rustc is found + if (!grepl("cargo", sysreqs, ignore.case = TRUE)) { + stop("You must specify `Cargo (Rust's package manager)` in your `SystemRequirements`") + } + + if (!grepl("rustc", sysreqs, ignore.case = TRUE)) { + stop("You must specify `Cargo (Rust's package manager), rustc` in your `SystemRequirements`") + } + + # split into parts + parts <- strsplit(sysreqs, ", ")[[1]] + + # identify which is the rustc + rustc_ver <- parts[grepl("rustc", parts)] + + # perform checks for the presence of rustc and cargo on the OS + no_cargo_msg <- c( + "----------------------- [CARGO NOT FOUND]--------------------------", + "The 'cargo' command was not found on the PATH. Please install Cargo", + "from: https://www.rust-lang.org/tools/install", + "", + "Alternatively, you may install Cargo from your OS package manager:", + " - Debian/Ubuntu: apt-get install cargo", + " - Fedora/CentOS: dnf install cargo", + " - macOS: brew install rustc", + "-------------------------------------------------------------------" + ) + + no_rustc_msg <- c( + "----------------------- [RUST NOT FOUND]---------------------------", + "The 'rustc' compiler was not found on the PATH. Please install", + paste(rustc_ver, "or higher from:"), + "https://www.rust-lang.org/tools/install", + "", + "Alternatively, you may install Rust from your OS package manager:", + " - Debian/Ubuntu: apt-get install rustc", + " - Fedora/CentOS: dnf install rustc", + " - macOS: brew install rustc", + "-------------------------------------------------------------------" + ) + + # Add {user}/.cargo/bin to path before checking + new_path <- paste0( + Sys.getenv("PATH"), + ":", + paste0(Sys.getenv("HOME"), "/.cargo/bin") + ) + + # set the path with the new path + Sys.setenv("PATH" = new_path) + + # check for rustc installation + rustc_version <- tryCatch( + system("rustc --version", intern = TRUE), + error = function(e) { + stop(paste(no_rustc_msg, collapse = "\n")) + } + ) + + # check for cargo installation + cargo_version <- tryCatch( + system("cargo --version", intern = TRUE), + error = function(e) { + stop(paste(no_cargo_msg, collapse = "\n")) + } + ) + + # helper function to extract versions + extract_semver <- function(ver) { + if (grepl("\\d+\\.\\d+(\\.\\d+)?", ver)) { + sub(".*?(\\d+\\.\\d+(\\.\\d+)?).*", "\\1", ver) + } else { + NA + } + } + + # get the MSRV + msrv <- extract_semver(rustc_ver) + + # extract current version + current_rust_version <- extract_semver(rustc_version) + + # perform check + if (!is.na(msrv)) { + # -1 when current version is later + # 0 when they are the same + # 1 when MSRV is newer than current + is_msrv <- utils::compareVersion(msrv, current_rust_version) + if (is_msrv == 1) { + fmt <- paste0( + "\n------------------ [UNSUPPORTED RUST VERSION]------------------\n", + "- Minimum supported Rust version is %s.\n", + "- Installed Rust version is %s.\n", + "---------------------------------------------------------------" + ) + stop(sprintf(fmt, msrv, current_rust_version)) + } + } + + # print the versions + versions_fmt <- "Using %s\nUsing %s" + message(sprintf(versions_fmt, cargo_version, rustc_version)) --- @@ -41,7 +203,18 @@ --- Code - cat_file("src", "Makevars") + cat_file("src", ".gitignore") + Output + *.o + *.so + *.dll + target + .cargo + +--- + + Code + cat_file("src", "Makevars.in") Output TARGET_DIR = ./rust/target LIBDIR = $(TARGET_DIR)/release @@ -53,31 +226,59 @@ $(SHLIB): $(STATLIB) CARGOTMP = $(CURDIR)/.cargo + VENDOR_DIR = $(CURDIR)/vendor + + # RUSTFLAGS appends --print=native-static-libs to ensure that + # the correct linkers are used. Use this for debugging if need. + # + # CRAN note: Cargo and Rustc versions are reported during + # configure via tools/msrv.R. + # + # When the NOT_CRAN flag is *not* set, the vendor.tar.xz, if present, + # is unzipped and used for offline compilation. $(STATLIB): - # In some environments, ~/.cargo/bin might not be included in PATH, so we need - # to set it here to ensure cargo can be invoked. It is appended to PATH and - # therefore is only used if cargo is absent from the user's PATH. - if [ "$(NOT_CRAN)" != "true" ]; then \ - export CARGO_HOME=$(CARGOTMP); \ - fi && \ - export PATH="$(PATH):$(HOME)/.cargo/bin" && \ - cargo build --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + + # Check if NOT_CRAN is false and unzip vendor.tar.xz if so if [ "$(NOT_CRAN)" != "true" ]; then \ - rm -Rf $(CARGOTMP) && \ - rm -Rf $(LIBDIR)/build; \ + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi; \ fi + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + C_clean: rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) + +--- + + Code + cat_file("src", "entrypoint.c") + Output + // We need to forward routine registration from C to Rust + // to avoid the linker removing the static library. + + void R_init_testpkg_extendr(void *dll); + + void R_init_testpkg(void *dll) { + R_init_testpkg_extendr(dll); + } --- Code - cat_file("src", "Makevars.win") + cat_file("src", "Makevars.win.in") Output TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu @@ -102,18 +303,26 @@ # https://github.com/r-windows/rtools-packages/blob/2407b23f1e0925bbb20a4162c963600105236318/mingw-w64-gcc/PKGBUILD#L313-L316 touch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a - # CARGO_LINKER is provided in Makevars.ucrt for R >= 4.2 - if [ "$(NOT_CRAN)" != "true" ]; then \ - export CARGO_HOME=$(CARGOTMP); \ - fi && \ - export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \ - export LIBRARY_PATH="$${LIBRARY_PATH};$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ - cargo build --target=$(TARGET) --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + # When the NOT_CRAN flag is *not* set, the vendor.tar.xz, if present, + # is unzipped and used for offline compilation. if [ "$(NOT_CRAN)" != "true" ]; then \ - rm -Rf $(CARGOTMP) && \ - rm -Rf $(LIBDIR)/build; \ + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi; \ fi + # CARGO_LINKER is provided in Makevars.ucrt for R >= 4.2 + # Build the project using Cargo with additional flags + export CARGO_HOME=$(CARGOTMP) && \ + export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \ + export LIBRARY_PATH="$${LIBRARY_PATH};$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --target=$(TARGET) --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + C_clean: rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) @@ -131,20 +340,6 @@ include Makevars.win ---- - - Code - cat_file("src", "entrypoint.c") - Output - // We need to forward routine registration from C to Rust - // to avoid the linker removing the static library. - - void R_init_testpkg_extendr(void *dll); - - void R_init_testpkg(void *dll) { - R_init_testpkg_extendr(dll); - } - --- Code @@ -204,14 +399,17 @@ use_extendr() Message > File 'src/entrypoint.c' already exists. Skip writing the file. - > File 'src/Makevars' already exists. Skip writing the file. - > File 'src/Makevars.win' already exists. Skip writing the file. + > File 'src/Makevars.in' already exists. Skip writing the file. + > File 'src/Makevars.win.in' already exists. Skip writing the file. > File 'src/Makevars.ucrt' already exists. Skip writing the file. > File 'src/.gitignore' already exists. Skip writing the file. > File 'src/rust/Cargo.toml' already exists. Skip writing the file. > File 'src/rust/src/lib.rs' already exists. Skip writing the file. > File 'src/testpkg.wrap-win.def' already exists. Skip writing the file. > File 'R/extendr-wrappers.R' already exists. Skip writing the file. + > File 'tools/msrv.R' already exists. Skip writing the file. + > File 'configure' already exists. Skip writing the file. + > File 'configure.win' already exists. Skip writing the file. v Finished configuring extendr for package testpkg.wrap. * Please run `rextendr::document()` for changes to take effect. @@ -221,14 +419,17 @@ use_extendr(crate_name = "foo", lib_name = "bar", overwrite = TRUE) Message v Writing 'src/entrypoint.c' - v Writing 'src/Makevars' - v Writing 'src/Makevars.win' + v Writing 'src/Makevars.in' + v Writing 'src/Makevars.win.in' v Writing 'src/Makevars.ucrt' v Writing 'src/.gitignore' v Writing 'src/rust/Cargo.toml' v Writing 'src/rust/src/lib.rs' v Writing 'src/testpkg-win.def' > File 'R/extendr-wrappers.R' already exists. Skip writing the file. + v Writing 'tools/msrv.R' + v Writing 'configure' + v Writing 'configure.win' v Finished configuring extendr for package testpkg. * Please run `rextendr::document()` for changes to take effect. @@ -253,7 +454,7 @@ # use_rextendr_template() can overwrite existing files Code - cat_file("src", "Makevars") + cat_file("src", "Makevars.in") Output TARGET_DIR = ./rust/target LIBDIR = $(TARGET_DIR)/release @@ -265,24 +466,38 @@ $(SHLIB): $(STATLIB) CARGOTMP = $(CURDIR)/.cargo + VENDOR_DIR = $(CURDIR)/vendor + + # RUSTFLAGS appends --print=native-static-libs to ensure that + # the correct linkers are used. Use this for debugging if need. + # + # CRAN note: Cargo and Rustc versions are reported during + # configure via tools/msrv.R. + # + # When the NOT_CRAN flag is *not* set, the vendor.tar.xz, if present, + # is unzipped and used for offline compilation. $(STATLIB): - # In some environments, ~/.cargo/bin might not be included in PATH, so we need - # to set it here to ensure cargo can be invoked. It is appended to PATH and - # therefore is only used if cargo is absent from the user's PATH. - if [ "$(NOT_CRAN)" != "true" ]; then \ - export CARGO_HOME=$(CARGOTMP); \ - fi && \ - export PATH="$(PATH):$(HOME)/.cargo/bin" && \ - cargo build --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + + # Check if NOT_CRAN is false and unzip vendor.tar.xz if so if [ "$(NOT_CRAN)" != "true" ]; then \ - rm -Rf $(CARGOTMP) && \ - rm -Rf $(LIBDIR)/build; \ + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi; \ fi + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --lib --release --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + C_clean: rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) clean: - rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index d5f58ee6..c94d89d1 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -112,3 +112,16 @@ skip_if_opted_out_of_dev_tests <- function() { testthat::skip("Dev extendr tests disabled") } } + +#' Mask any version in snapshot files +#' @param snapshot_lines Character vector, lines of the snapshot file +#' @example +#' expect_snapshot(some_operation(), transform = mask_any_version) +#' @noRd +mask_any_version <- function(snapshot_lines) { + stringi::stri_replace_all_regex( + snapshot_lines, + "\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?", + "*.*.*" + ) +} diff --git a/tests/testthat/test-clean.R b/tests/testthat/test-clean.R index 3ee022ee..b20c8354 100644 --- a/tests/testthat/test-clean.R +++ b/tests/testthat/test-clean.R @@ -11,8 +11,15 @@ test_that("rextendr::clean() removes cargo target directory & binaries", { expect_equal(length(dir("src", pattern = "testpkg\\..*")), 1) expect_true(dir.exists(file.path("src", "rust", "target"))) + + # clean once clean() + # we expect an error the second time + expect_error(clean()) + + expect_error(clean(1L)) + expect_error(clean(echo = NULL)) expect_equal(length(dir("src", pattern = "testpkg\\..*")), 0) expect_false(dir.exists(file.path("src", "rust", "target"))) }) diff --git a/tests/testthat/test-cran-compliance.R b/tests/testthat/test-cran-compliance.R new file mode 100644 index 00000000..11e1293e --- /dev/null +++ b/tests/testthat/test-cran-compliance.R @@ -0,0 +1,46 @@ +test_that("vendor_pkgs() vendors dependencies", { + skip_if_not_installed("usethis") + + path <- local_package("testpkg") + + expect_error(vendor_pkgs(path)) + # capture setup messages + withr::local_options(usethis.quiet = FALSE) + use_extendr(path, quiet = TRUE) + + package_versions <- vendor_pkgs(path, quiet = TRUE) + expect_snapshot(cat_file("src", "rust", "vendor-config.toml")) + expect_snapshot(package_versions, transform = mask_any_version) + expect_true(file.exists(file.path("src", "rust", "vendor.tar.xz"))) +}) + + +test_that("rextendr passes CRAN checks", { + skip_if_not_installed("usethis") + skip_if_not_installed("rcmdcheck") + + path <- local_package("testpkg") + # write the license file to pass R CMD check + usethis::use_mit_license() + use_extendr() + document() + vendor_pkgs() + + res <- rcmdcheck::rcmdcheck( + env = c("NOT_CRAN" = ""), + args = "--no-manual", + libpath = rev(.libPaths()) + ) + + # --offline flag should be set + expect_true(grepl("--offline", res$install_out)) + # -j 2 flag should be set + expect_true(grepl("-j 2", res$install_out)) + + # "Downloading" should not be present + expect_false(grepl("Downloading", res$install_out)) + + expect_true( + rlang::is_empty(res$errors) && rlang::is_empty(res$warnings) + ) +}) diff --git a/tests/testthat/test-eval.R b/tests/testthat/test-eval.R index bb9dcb20..45bbdc61 100644 --- a/tests/testthat/test-eval.R +++ b/tests/testthat/test-eval.R @@ -20,12 +20,12 @@ test_that("multiple `rust_eval_deferred()` work correctly", { skip_if_cargo_unavailable() provided_values <- seq_len(5) - deferred_handles <- purrr::map( + deferred_handles <- map( provided_values, ~ rust_eval_deferred(glue::glue("{.x}i32")) ) - obtained_values <- purrr::map_int(deferred_handles, ~ (.x)()) + obtained_values <- map_int(deferred_handles, ~ (.x)()) testthat::expect_equal( obtained_values, @@ -48,14 +48,14 @@ test_that("multiple `rust_eval_deferred()` work correctly in reverse order", { provided_values <- seq_len(5) - deferred_handles <- purrr::map( + deferred_handles <- map( provided_values, ~ rust_eval_deferred(glue::glue("{.x}i32")) ) deferred_handles <- rev(deferred_handles) - obtained_values <- purrr::map_int(deferred_handles, ~ (.x)()) + obtained_values <- map_int(deferred_handles, ~ (.x)()) testthat::expect_equal( obtained_values, @@ -99,13 +99,13 @@ test_that("`rust_eval_deferred()` environment cleanup", { dll_path <- attr(handle, "dll_path") testthat::expect_true(exists(fn_name)) - dlls <- purrr::keep(getLoadedDLLs(), ~ .x[["path"]] == dll_path) + dlls <- keep(getLoadedDLLs(), ~ .x[["path"]] == dll_path) testthat::expect_length(dlls, 1L) testthat::expect_equal(handle(), 42L) testthat::expect_false(exists(fn_name)) - dlls <- purrr::keep(getLoadedDLLs(), ~ .x[["path"]] == dll_path) + dlls <- keep(getLoadedDLLs(), ~ .x[["path"]] == dll_path) testthat::expect_length(dlls, 0L) }) diff --git a/tests/testthat/test-find_extendr.R b/tests/testthat/test-find_extendr.R new file mode 100644 index 00000000..d8a85177 --- /dev/null +++ b/tests/testthat/test-find_extendr.R @@ -0,0 +1,33 @@ +test_that("find_extendr_crate() returns path to Rust crate", { + skip_if_not_installed("usethis") + + path <- local_package("testpkg") + + # capture setup messages + withr::local_options(usethis.quiet = FALSE) + + expect_error(find_extendr_crate(), class = "rextendr_error") + + use_extendr(path, quiet = TRUE) + + rust_folder <- find_extendr_crate() + + expect_true(dir.exists(rust_folder)) +}) + +test_that("find_extendr_manifest() returns path to Cargo manifest", { + skip_if_not_installed("usethis") + + path <- local_package("testpkg") + + # capture setup messages + withr::local_options(usethis.quiet = FALSE) + + expect_error(find_extendr_manifest(), class = "rextendr_error") + + use_extendr(path, quiet = TRUE) + + manifest_path <- find_extendr_manifest() + + expect_true(file.exists(manifest_path)) +}) diff --git a/tests/testthat/test-license_note.R b/tests/testthat/test-license_note.R index 280cb870..92bf73cb 100644 --- a/tests/testthat/test-license_note.R +++ b/tests/testthat/test-license_note.R @@ -3,10 +3,15 @@ test_that("LICENSE.note is generated properly", { skip_if_cargo_unavailable(c("license", "--help")) local_package("testPackage") + + # try running write_license_note() when there is nothing present + dir.create(file.path("src", "rust"), recursive = TRUE) + expect_error(write_license_note()) + + # create license note for extendr package use_extendr() write_license_note() - expect_snapshot(cat_file("LICENSE.note")) - expect_rextendr_error(write_license_note(), NA) - expect_rextendr_error(write_license_note(force = FALSE), "LICENSE.note already exists.") + expect_error(write_license_note(path = NULL)) + expect_error(write_license_note(force = "yup")) }) diff --git a/tests/testthat/test-read_cargo_metadata.R b/tests/testthat/test-read_cargo_metadata.R new file mode 100644 index 00000000..6ec99b6e --- /dev/null +++ b/tests/testthat/test-read_cargo_metadata.R @@ -0,0 +1,38 @@ +test_that("read_cargo_metadata() returns crate or workspace metadata", { + skip_if_not_installed("usethis") + + path <- local_package("testpkg") + + # capture setup messages + withr::local_options(usethis.quiet = FALSE) + + use_extendr(path, quiet = TRUE) + + out <- read_cargo_metadata(path) + + expect_type(out, "list") + + expect_equal( + out[["packages"]][["name"]], + "testpkg" + ) + + expect_equal( + out[["packages"]][["version"]], + "0.1.0" + ) + + expect_equal( + out[["packages"]][["dependencies"]][[1]][["name"]], + "extendr-api" + ) + + expect_equal( + out[["workspace_root"]], + normalizePath( + file.path(path, "src", "rust"), + winslash = "\\", + mustWork = FALSE + ) + ) +}) diff --git a/tests/testthat/test-rstudio-template.R b/tests/testthat/test-rstudio-template.R new file mode 100644 index 00000000..3c3157ad --- /dev/null +++ b/tests/testthat/test-rstudio-template.R @@ -0,0 +1,25 @@ +test_that("RStudio template generation is correct", { + pkg_name <- "extendrtest" + tmp <- file.path(tempdir(), pkg_name) + + pkg <- create_extendr_package( + tmp, + roxygen = TRUE, + check_name = FALSE, + crate_name = pkg_name, + lib_name = pkg_name, + edition = "2021" + ) + + expected_files <- c( + "configure", "configure.win", "DESCRIPTION", + "extendrtest.Rproj", "NAMESPACE", "R/extendr-wrappers.R", + "src/entrypoint.c", "src/extendrtest-win.def", + "src/Makevars.in", "src/Makevars.ucrt", "src/Makevars.win.in", + "src/rust/Cargo.toml", "src/rust/src/lib.rs", "tools/msrv.R" + ) + + for (file in expected_files) { + expect_true(file.exists(file.path(tmp, file))) + } +}) diff --git a/tests/testthat/test-use_cran_defaults.R b/tests/testthat/test-use_cran_defaults.R deleted file mode 100644 index b3ba8764..00000000 --- a/tests/testthat/test-use_cran_defaults.R +++ /dev/null @@ -1,49 +0,0 @@ -test_that("use_cran_defaults() modifies and creates files correctly", { - skip_if_not_installed("usethis") - - path <- local_package("testpkg") - # capture setup messages - withr::local_options(usethis.quiet = FALSE) - expect_snapshot(use_extendr()) - expect_snapshot(use_cran_defaults()) - - expect_snapshot(cat_file("src", "Makevars")) - expect_snapshot(cat_file("src", "Makevars.win")) - expect_snapshot(cat_file("configure")) - expect_snapshot(cat_file("configure.win")) - expect_snapshot(cat_file("tools", "msrv.R")) -}) - -test_that("use_cran_defaults() quiet if quiet=TRUE", { - skip_if_not_installed("usethis") - - path <- local_package("quiet") - expect_snapshot({ - use_extendr(quiet = TRUE) - use_cran_defaults(quiet = TRUE) - }) -}) - - -test_that("vendor_pkgs() vendors dependencies", { - mask_version_strings <- function(snapshot_lines) { - stringi::stri_replace_all_regex( - snapshot_lines, - "\\d+\\.\\d+\\.\\d+", - "*.*.*" - ) - } - - skip_if_not_installed("usethis") - - path <- local_package("testpkg") - # capture setup messages - withr::local_options(usethis.quiet = FALSE) - use_extendr(path, quiet = TRUE) - use_cran_defaults(path, quiet = TRUE, overwrite = TRUE) - - package_versions <- vendor_pkgs(path, quiet = TRUE) - expect_snapshot(cat_file("src", "rust", "vendor-config.toml")) - expect_snapshot(package_versions, transform = mask_version_strings) - expect_true(file.exists(file.path("src", "rust", "vendor.tar.xz"))) -}) diff --git a/tests/testthat/test-use_crate.R b/tests/testthat/test-use_crate.R new file mode 100644 index 00000000..8f7328ed --- /dev/null +++ b/tests/testthat/test-use_crate.R @@ -0,0 +1,96 @@ +test_that("use_crate() adds dependency to package or workspace", { + skip_if_not_installed("usethis") + + path <- local_package("testpkg") + + # capture setup messages + withr::local_options(usethis.quiet = FALSE) + + use_extendr(path, quiet = TRUE) + + use_crate( + "serde", + features = "derive", + version = "1.0.1", + path = path + ) + + metadata <- read_cargo_metadata(path, echo = FALSE) + + dependency <- metadata[["packages"]][["dependencies"]][[1]] + dependency <- dependency[dependency[["name"]] == "serde", ] + + expect_equal(dependency[["name"]], "serde") + expect_equal(dependency[["features"]][[1]], "derive") + expect_equal(dependency[["req"]], "^1.0.1") + +}) + +test_that("use_crate() errors when user passes git and version arguments", { + skip_if_not_installed("usethis") + + path <- local_package("testpkg") + + # capture setup messages + withr::local_options(usethis.quiet = FALSE) + + use_extendr(path, quiet = TRUE) + + fn <- function() { + use_crate( + "serde", + git = "https://github.com/serde-rs/serde", + version = "1.0.1" + ) + } + + expect_error(fn(), class = "rextendr_error") +}) + +test_that("use_crate(optional = TRUE) adds optional dependency", { + skip_if_not_installed("usethis") + + path <- local_package("testpkg") + + # capture setup messages + withr::local_options(usethis.quiet = FALSE) + + use_extendr(path, quiet = TRUE) + + use_crate( + "serde", + optional = TRUE, + path = path + ) + + metadata <- read_cargo_metadata(path) + + dependency <- metadata[["packages"]][["dependencies"]][[1]] + dependency <- dependency[dependency[["name"]] == "serde", ] + + expect_identical(dependency[["optional"]], TRUE) +}) + +test_that("use_crate(git = ) adds dependency with git source", { + skip_if_not_installed("usethis") + + path <- local_package("testpkg") + + # capture setup messages + withr::local_options(usethis.quiet = FALSE) + + use_extendr(path, quiet = TRUE) + + use_crate( + "serde", + git = "https://github.com/serde-rs/serde", + path = path + ) + + metadata <- read_cargo_metadata(path) + + dependency <- metadata[["packages"]][["dependencies"]][[1]] + dependency <- dependency[dependency[["name"]] == "serde", ] + + expect_equal(dependency[["source"]], "git+https://github.com/serde-rs/serde") +}) diff --git a/tests/testthat/test-use_extendr.R b/tests/testthat/test-use_extendr.R index 124cea6a..52f1f236 100644 --- a/tests/testthat/test-use_extendr.R +++ b/tests/testthat/test-use_extendr.R @@ -4,24 +4,32 @@ test_that("use_extendr() sets up extendr files correctly", { path <- local_package("testpkg") # capture setup messages withr::local_options(usethis.quiet = FALSE) - expect_snapshot(use_extendr()) + use_extendr() # DESCRITION file version_in_desc <- stringi::stri_trim_both(desc::desc_get("Config/rextendr/version", path)[[1]]) sysreq_in_desc <- stringi::stri_trim_both(desc::desc_get("SystemRequirements", path)[[1]]) - expect_equal(version_in_desc, as.character(packageVersion("rextendr"))) - expect_equal(sysreq_in_desc, "Cargo (Rust's package manager), rustc") + expect_identical(version_in_desc, as.character(packageVersion("rextendr"))) + expect_identical(sysreq_in_desc, "Cargo (Rust's package manager), rustc") # directory structure expect_true(dir.exists("src")) + expect_true(dir.exists("tools")) expect_true(dir.exists(file.path("src", "rust"))) expect_true(dir.exists(file.path("src", "rust", "src"))) + # ensure all files generated by rextendr are present + expect_snapshot(cat_file(".gitignore")) + expect_snapshot(cat_file(".Rbuildignore")) + expect_snapshot(cat_file("configure")) + expect_snapshot(cat_file("configure.win")) + expect_snapshot(cat_file("tools", "msrv.R")) expect_snapshot(cat_file("R", "extendr-wrappers.R")) - expect_snapshot(cat_file("src", "Makevars")) - expect_snapshot(cat_file("src", "Makevars.win")) - expect_snapshot(cat_file("src", "Makevars.ucrt")) + expect_snapshot(cat_file("src", ".gitignore")) + expect_snapshot(cat_file("src", "Makevars.in")) expect_snapshot(cat_file("src", "entrypoint.c")) + expect_snapshot(cat_file("src", "Makevars.win.in")) + expect_snapshot(cat_file("src", "Makevars.ucrt")) expect_snapshot(cat_file("src", "testpkg-win.def")) expect_snapshot(cat_file("src", "rust", "Cargo.toml")) expect_snapshot(cat_file("src", "rust", "src", "lib.rs")) @@ -87,23 +95,23 @@ test_that("use_rextendr_template() can overwrite existing files", { path <- local_package("testpkg.wrap") dir.create("src") - file_path <- file.path("src", "Makevars") + file_path <- file.path("src", "Makevars.in") use_rextendr_template( - "Makevars", + "Makevars.in", save_as = file_path, quiet = TRUE, data = list(lib_name = "foo") ) use_rextendr_template( - "Makevars", + "Makevars.in", save_as = file_path, quiet = TRUE, overwrite = TRUE, data = list(lib_name = "bar") ) - expect_snapshot(cat_file("src", "Makevars")) + expect_snapshot(cat_file("src", "Makevars.in")) }) # Check that {rextendr} works in packages containing dots in their names. @@ -119,7 +127,7 @@ test_that("use_extendr() handles R packages with dots in the name", { use_extendr() document() devtools::load_all() - expect_equal(hello_world(), "Hello world!") + expect_identical(hello_world(), "Hello world!") }) # Specify crate name and library names explicitly @@ -133,7 +141,7 @@ test_that("use_extendr() handles R package name, crate name and library name sep use_extendr(crate_name = "crate_name", lib_name = "lib_name") document() devtools::load_all() - expect_equal(hello_world(), "Hello world!") + expect_identical(hello_world(), "Hello world!") }) # Pass unsupported values to `crate_name` and `lib_name` and expect errors. @@ -173,7 +181,7 @@ test_that("Message if the SystemRequirements field is already set.", { ) expect_true(created) - expect_equal(desc::desc_get("SystemRequirements")[[1]], sys_req) + expect_identical(desc::desc_get("SystemRequirements")[[1]], sys_req) }) test_that("`use_extendr()` works correctly when path is specified explicitly", { @@ -184,3 +192,26 @@ test_that("`use_extendr()` works correctly when path is specified explicitly", { use_extendr(path = "testpkg") succeed() }) + + +test_that("`use_extendr()` passes R CMD check", { + skip_if_not_installed("usethis") + skip_if_not_installed("rcmdcheck") + + path <- local_package("testpkg") + # write the license file to pass R CMD check + usethis::use_mit_license() + use_extendr() + document() + + # store results + res <- rcmdcheck::rcmdcheck( + args = "--no-manual", + libpath = rev(.libPaths()) + ) + + # check the output + expect_true( + rlang::is_empty(res$errors) && rlang::is_empty(res$warnings) + ) +}) diff --git a/tests/testthat/test-use_msrv.R b/tests/testthat/test-use_msrv.R index 9fc128d2..29dcc83c 100644 --- a/tests/testthat/test-use_msrv.R +++ b/tests/testthat/test-use_msrv.R @@ -7,7 +7,7 @@ test_that("use_msrv() modifies the MSRV in the DESCRIPTION", { withr::local_options(usethis.quiet = FALSE) use_extendr(path, quiet = TRUE) - use_msrv("1.70", path) + expect_no_error(use_msrv("1.70", path)) d <- desc::desc("DESCRIPTION") @@ -15,4 +15,14 @@ test_that("use_msrv() modifies the MSRV in the DESCRIPTION", { "Cargo (Rust's package manager), rustc >= 1.70", d$get_field("SystemRequirements") ) + + expect_error(use_msrv("adksfghu", path)) + + expect_error(use_msrv("1.70", path = "../doesntexist")) + + # when overwrite is FALSE and SystemRequirements is already set + expect_message( + use_msrv("1.65", overwrite = FALSE), + "The SystemRequirements field in the " + ) }) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index bc45a226..8f975eeb 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -20,3 +20,16 @@ test_that("`try_exec_cmd()` returns stdout when command is available", { echo <- "This is an echo" expect_equal(try_exec_cmd("echo", echo), echo) }) + + +test_that("`replace_na()` respects type", { + x <- 1:5 + x[2] <- NA + expect_error(replace_na(x, "L")) +}) + +test_that("`replace_na()` replaces with the correct value", { + x <- 1:5 + x[2] <- NA_integer_ + expect_identical(replace_na(x, -99L), c(1L, -99L, 3L, 4L, 5L)) +}) diff --git a/vignettes/articles/cran-compliance.Rmd b/vignettes/articles/cran-compliance.Rmd deleted file mode 100644 index ba43272b..00000000 --- a/vignettes/articles/cran-compliance.Rmd +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: "CRAN compliant extendr packages" -author: "Josiah Parry" ---- - -```{r, include = FALSE} -knitr::opts_chunk$set( - collapse = TRUE, - comment = "#>" -) -``` - -In order for Rust-based packages to exist on CRAN, there are a number of -fairly stringent requirements that must be adhered to. CRAN published [Using Rust in CRAN packages](https://cran.r-project.org/web/packages/using_rust.html) in mid-2023, outlining their requirements for building and hosting Rust-based packages. - -This article describes CRAN requirements as of the day of writing and illustrates how `{rextendr}` can be used to adhere to them. - -## `SystemRequirements` - -Building Rust-backed packages from source requires the system dependencies `cargo` and `rustc`. CRAN has stipulated their preferred way of tracking this is using the following line in a packages `DESCRIPTION` file. - -``` -SystemRequirements: Cargo (Rust's package manager), rustc -``` -Even though this is a free-form field, having consistency can help the whole ecosystem keep track of Rust-based R packages. - -## `cargo` and `rustc` availability - -In order for an R package to be built from source, `cargo` and `rustc` need to be available to the machine compiling the package. The expectation for R packages using external dependencies is to have a `configure` and `configure.win` files that check if the dependencies are available before attempting to compile the package. If the checks fail, the build process will be stopped prematurely. - -CRAN expects that if `cargo` is not on the `PATH`, the user's home directory is checked at `~/.cargo/bin`. The configuration files must perform these checks. - -## `cargo build` settings - -CRAN also imposes restrictions on how `cargo` builds crates. CRAN has requested that no more than two logical CPUs be used in the build process. By default, `cargo` uses multiple threads to speed up the compilation process. CRAN policy allows for a maximum of two. This is set using the `-j 2` option, which is passed to `cargo build`. - -Additionally, to minimize security risks and ensure package stability, CRAN requires that packages be built completely offline. This prevents external dependencies from being downloaded at compile time. Because of this requirement, vendored dependencies must be used. - -## Vendored dependencies - -Vendoring dependencies is the act of including the dependency itself in a package source code. In the case of Rust, dependencies are fetched only at compile time. To enable compilation in an offline environment, dependencies must be vendored, which is accomplished using the `cargo vendor` command. - -`cargo vendor` creates a local directory with the default name `vendor`, which contains the source code for each of the recursive dependencies of the crate that is being built. For CRAN compatibility, the `vendor` directory must be compressed using tar xz compression and included in the source of the package. - -During the build time, the dependencies are extracted, compiled, and then discarded. This process is controlled by the `Makevars` and `Makevars.win` files. - -## Package compilation - -All of this comes together during package compilation time, providing all of the following requirements are met: - -- cargo must be able to be called from a user's home directory -- the user's home directory must not be modified or written to -- the package must be compiled offline -- no more than two logical CPUs are used -- the versions of `cargo` and `rustc` are printed - - -## Using CRAN defaults - -rextendr provides default CRAN compliant scaffolding via the `use_cran_defaults()` function and appropriate vendoring with `vendor_pkgs()`. - -### Making a package CRAN compliant - -To create a CRAN compliant R package begin by creating a new R package. Do so by calling `usethis::create_package()`. In the new R project, run `rextendr::use_extendr()` to create the minimal scaffolding necessary for a Rust-powered R package. Once you have done this, you can now run `rextendr::use_cran_defaults()`. - -`use_cran_defaults()` will create the `configure` and `configure.win` files. Additionally, it will create new `Makevars` and `Makevars.win` that print the versions of `cargo` and `rustc` as well as use the `cargo build` argument `-j 2 --offline`. - -### Vendoring packages - -After having configured your R package to use CRAN defaults, you will need to vendor your dependencies. - -`vendor_pkgs()` runs `cargo vendor` on your behalf, compresses the `vendor/` directory, and updates the `vendor-config.toml` file accordingly. - -When you have added new dependencies, changed the version or source of the crates, you should use `vendor_pkgs()` again. Doing so ensures that the compressed `vendor.tar.xz` contains the updates too. This is very important for CI and publishing to CRAN.