diff --git a/.Rbuildignore b/.Rbuildignore index 15e32d8b..49da0119 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -17,3 +17,4 @@ ^\.idea$ ^inst/libgcc_mock/libgcc_eh.a$ ^CRAN-SUBMISSION$ +^vignettes/articles$ diff --git a/NAMESPACE b/NAMESPACE index 4232e168..81fec799 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -11,7 +11,9 @@ export(rust_function) export(rust_sitrep) export(rust_source) export(to_toml) +export(use_cran_defaults) export(use_extendr) +export(vendor_pkgs) export(write_license_note) importFrom(dplyr,"%>%") importFrom(dplyr,mutate) diff --git a/NEWS.md b/NEWS.md index 6601ab21..f0c904a9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # rextendr (development version) +* 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). * `use_extendr()` and `document()` now set the `SystemRequirements` field of the `DESCRIPTION` file to `Cargo (rustc package manager)` if the field is empty (#298). diff --git a/R/cran-compliance.R b/R/cran-compliance.R new file mode 100644 index 00000000..6fef5b36 --- /dev/null +++ b/R/cran-compliance.R @@ -0,0 +1,224 @@ +#' 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 `configure` and `configure.win` files as well as +#' modifies `Makevars` and `Makevars.win` to use required CRAN settings. +#' +#' `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()`. +#' +#' @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() +#' } +#' @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" + ) + } + + # 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)) { + cli::cat_bullet(stringi::stri_trim_left(x)) + } + } + local_quiet_cli(quiet) + + # get path to rust folder + src_dir <- rprojroot::find_package_root_file(path, "src/rust") + + # 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}?"), + class = "rextendr_error" + ) + } + + # if cargo.lock does not exist, cerate it using `cargo update` + cargo_lock_fp <- file.path(src_dir, "Cargo.lock") + + if (!file.exists(cargo_lock_fp)) { + withr::with_dir(src_dir, { + update_res <- processx::run( + "cargo", + c( + "generate-lockfile", + "--manifest-path", + file.path(src_dir, "Cargo.toml") + ), + stderr_line_callback = stderr_line_callback + ) + }) + + if (update_res[["status"]] != 0) { + cli::cli_abort( + "{.file Cargo.lock} could not be created using {.code cargo generate-lockfile}", + class = "rextendr_error" + ) + } + } + + # vendor crates + withr::with_dir(src_dir, { + vendor_res <- processx::run( + "cargo", + c( + "vendor", + "--locked", + "--manifest-path", + file.path(src_dir, "Cargo.toml") + ), + stderr_line_callback = stderr_line_callback + ) + }) + + if (vendor_res[["status"]] != 0) { + cli::cli_abort( + "{.code cargo vendor} failed", + class = "rextendr_error" + ) + } + + # create a dataframe of vendored crates + vendored <- vendor_res[["stderr"]] %>% + cli::ansi_strip() %>% + stringi::stri_split_lines1() + + res <- stringi::stri_match_first_regex(vendored, "Vendoring\\s([A-z0-9_][A-z0-9_-]*?)\\s[vV](.+?)(?=\\s)") %>% + tibble::as_tibble(.name_repair = "minimal") %>% + rlang::set_names(c("source", "crate", "version")) %>% + dplyr::filter(!is.na(source)) %>% + dplyr::select(-source) %>% + dplyr::arrange(crate) # nolint: object_usage_linter + + # capture vendor-config.toml content + config_toml <- vendor_res[["stdout"]] %>% + cli::ansi_strip() %>% + stringi::stri_split_lines1() + + # always write to file as cargo vendor catches things like patch.crates-io + # and provides the appropriate configuration. + brio::write_lines(config_toml, file.path(src_dir, "vendor-config.toml")) + cli::cli_alert_info("Writing {.file src/rust/vendor-config.toml}") + + # compress to vendor.tar.xz + compress_res <- withr::with_dir(src_dir, { + processx::run( + "tar", c( + "-cJ", "--no-xattrs", "-f", "vendor.tar.xz", "vendor" + ) + ) + }) + + if (compress_res[["status"]] != 0) { + cli::cli_abort( + "Folder {.path vendor} could not be compressed", + class = "rextendr_error" + ) + } + + # return packages and versions invisibly + invisible(res) +} diff --git a/R/features.R b/R/features.R index 8a32f21e..e4552398 100644 --- a/R/features.R +++ b/R/features.R @@ -6,10 +6,12 @@ validate_extendr_features <- function(features, suppress_warnings) { features <- features %||% character(0) if (!vctrs::vec_is(features, character())) { - cli::cli_abort(c( - "!" = "{.arg features} expected to be a vector of type {.cls character}, but got {.cls {class(features)}}." - ), - class = "rextendr_error") + cli::cli_abort( + c( + "!" = "{.arg features} expected to be a vector of type {.cls character}, but got {.cls {class(features)}}." + ), + class = "rextendr_error" + ) } features <- unique(features) diff --git a/README.Rmd b/README.Rmd index fec1e188..95a3d9cb 100644 --- a/README.Rmd +++ b/README.Rmd @@ -41,7 +41,7 @@ remotes::install_cran("rextendr") You can also install `{rextendr}` from [r-universe](https://extendr.r-universe.dev/rextendr): ```{r, results = "hide"} -install.packages('rextendr', repos = c('https://extendr.r-universe.dev', 'https://cloud.r-project.org')) +install.packages("rextendr", repos = c("https://extendr.r-universe.dev", "https://cloud.r-project.org")) ``` Latest development version can be installed from GitHub: @@ -123,8 +123,8 @@ rust_function( Either::Right(x) => Either::Right(x.iter().sum()), } }", - use_dev_extendr = TRUE, # Use development version of extendr from GitHub - features = "either", # Enable support for Either crate + use_dev_extendr = TRUE, # Use development version of extendr from GitHub + features = "either", # Enable support for Either crate extendr_fn_options = list(use_try_from = TRUE) # Enable advanced type conversion ) @@ -139,7 +139,6 @@ tibble::tibble( SumRaw = purrr::flatten_dbl(Sum), ResultType = purrr::map_chr(Sum, typeof) ) - ``` The package also enables a new chunk type for knitr, `extendr`, which compiles and evaluates Rust code. For example, a code chunk such as this one: diff --git a/_pkgdown.yml b/_pkgdown.yml index 6a6cff6b..dabd49fe 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -20,6 +20,7 @@ reference: - register_extendr - write_license_note - clean + - cran - title: Various utility functions contents: diff --git a/inst/templates/cran/Makevars b/inst/templates/cran/Makevars new file mode 100644 index 00000000..e2dc513e --- /dev/null +++ b/inst/templates/cran/Makevars @@ -0,0 +1,36 @@ +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 new file mode 100644 index 00000000..f02ffd85 --- /dev/null +++ b/inst/templates/cran/Makevars.win @@ -0,0 +1,55 @@ +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 + +all: C_clean + +$(SHLIB): $(STATLIB) + +CRAN_FLAGS=-j 2 --offline +CARGOTMP = $(CURDIR)/.cargo + +$(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 new file mode 100644 index 00000000..93bc3a4c --- /dev/null +++ b/inst/templates/cran/configure @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +# https://github.com/eitsupi/prqlr/blob/main/configure +export PATH="$PATH:$HOME/.cargo/bin" + +if [ ! "$(command -v cargo)" ]; then + echo "----------------------- [RUST NOT FOUND]---------------------------" + echo "The 'cargo' command was not found on the PATH. Please install rustc" + echo "from: https://www.rust-lang.org/tools/install" + echo "" + echo "Alternatively, you may install cargo from your OS package manager:" + echo " - Debian/Ubuntu: apt-get install cargo" + echo " - Fedora/CentOS: dnf install cargo" + echo " - macOS: brew install rustc" + echo "-------------------------------------------------------------------" + echo "" + exit 1 +fi + +exit 0 + diff --git a/inst/templates/cran/configure.win b/inst/templates/cran/configure.win new file mode 100644 index 00000000..d9b66edb --- /dev/null +++ b/inst/templates/cran/configure.win @@ -0,0 +1,15 @@ +#!/bin/sh + +# https://github.com/eitsupi/prqlr/blob/main/configure.win +export PATH="$PATH:$HOME/.cargo/bin" + +if [ ! "$(command -v cargo)" ]; then + echo "----------------------- [RUST NOT FOUND]---------------------------" + echo "The 'cargo' command was not found on the PATH. Please install rustc" + echo "from: https://www.rust-lang.org/tools/install" + echo "-------------------------------------------------------------------" + echo "" + exit 1 +fi + +exit 0 diff --git a/man/cran.Rd b/man/cran.Rd new file mode 100644 index 00000000..1d773ef4 --- /dev/null +++ b/man/cran.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% 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) +} +\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.} + +\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{ +\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 +} +} +\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{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{ + +if (interactive()) { + use_cran_defaults() + vendor_pkgs() +} +} diff --git a/tests/testthat/_snaps/use_cran_defaults.md b/tests/testthat/_snaps/use_cran_defaults.md new file mode 100644 index 00000000..aa9b6355 --- /dev/null +++ b/tests/testthat/_snaps/use_cran_defaults.md @@ -0,0 +1,198 @@ +# 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.9000" in the 'DESCRIPTION' file. + i Setting `SystemRequirements` to "Cargo (rustc package manager)" 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 '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) rust/target + +--- + + 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 + + # https://github.com/eitsupi/prqlr/blob/main/configure + export PATH="$PATH:$HOME/.cargo/bin" + + if [ ! "$(command -v cargo)" ]; then + echo "----------------------- [RUST NOT FOUND]---------------------------" + echo "The 'cargo' command was not found on the PATH. Please install rustc" + echo "from: https://www.rust-lang.org/tools/install" + echo "" + echo "Alternatively, you may install cargo from your OS package manager:" + echo " - Debian/Ubuntu: apt-get install cargo" + echo " - Fedora/CentOS: dnf install cargo" + echo " - macOS: brew install rustc" + echo "-------------------------------------------------------------------" + echo "" + exit 1 + fi + + exit 0 + +--- + + Code + cat_file("configure.win") + Output + #!/bin/sh + + # https://github.com/eitsupi/prqlr/blob/main/configure.win + export PATH="$PATH:$HOME/.cargo/bin" + + if [ ! "$(command -v cargo)" ]; then + echo "----------------------- [RUST NOT FOUND]---------------------------" + echo "The 'cargo' command was not found on the PATH. Please install rustc" + echo "from: https://www.rust-lang.org/tools/install" + echo "-------------------------------------------------------------------" + echo "" + exit 1 + fi + + exit 0 + +# 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 0.6.0 + 2 extendr-macros 0.6.0 + 3 libR-sys 0.6.0 + 4 once_cell 1.19.0 + 5 paste 1.0.14 + 6 proc-macro2 1.0.70 + 7 quote 1.0.33 + 8 syn 2.0.42 + 9 unicode-ident 1.0.12 + diff --git a/tests/testthat/test-use_cran_defaults.R b/tests/testthat/test-use_cran_defaults.R new file mode 100644 index 00000000..83ee5ad3 --- /dev/null +++ b/tests/testthat/test-use_cran_defaults.R @@ -0,0 +1,40 @@ +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")) +}) + +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", { + 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) + expect_true(file.exists(file.path("src", "rust", "vendor.tar.xz"))) +}) diff --git a/vignettes/articles/cran-compliance.Rmd b/vignettes/articles/cran-compliance.Rmd new file mode 100644 index 00000000..ba43272b --- /dev/null +++ b/vignettes/articles/cran-compliance.Rmd @@ -0,0 +1,74 @@ +--- +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.