diff --git a/DESCRIPTION b/DESCRIPTION index 65722059..f3a90aef 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -57,6 +57,7 @@ Imports: withr Suggests: devtools, + rcmdcheck, knitr, lintr, rmarkdown, diff --git a/NAMESPACE b/NAMESPACE index 6154edb7..a5420ce8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -21,7 +21,6 @@ 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) diff --git a/NEWS.md b/NEWS.md index 8438f966..ecc18976 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,8 @@ # rextendr (development version) -* `Makevars` now prints linked static libraries at compile time +* `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). diff --git a/R/cran-compliance.R b/R/cran-compliance.R index 4ca7505f..57323a28 100644 --- a/R/cran-compliance.R +++ b/R/cran-compliance.R @@ -1,99 +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" - ) - } - - # 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)) { @@ -199,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/use_extendr.R b/R/use_extendr.R index 7c62d9c7..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), @@ -179,7 +177,7 @@ use_extendr <- function(path = ".", # add msrv.R template use_rextendr_template( - "cran/msrv.R", + "msrv.R", save_as = file.path("tools", "msrv.R"), quiet = quiet, overwrite = overwrite @@ -187,27 +185,45 @@ use_extendr <- function(path = ".", # add configure and configure.win templates use_rextendr_template( - "cran/configure", + "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") } - use_rextendr_template( - "cran/configure.win", - save_as = "configure.win", - quiet = quiet, - overwrite = overwrite, - data = list(lib_name = lib_name) + # 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}}.") diff --git a/_pkgdown.yml b/_pkgdown.yml index 74a0ad15..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 diff --git a/inst/templates/Makevars b/inst/templates/Makevars deleted file mode 100644 index 6ec006bf..00000000 --- a/inst/templates/Makevars +++ /dev/null @@ -1,33 +0,0 @@ -TARGET_DIR = ./rust/target -LIBDIR = $(TARGET_DIR)/release -STATLIB = $(LIBDIR)/lib{{{lib_name}}}.a -PKG_LIBS = -L$(LIBDIR) -l{{{lib_name}}} - -# Print linked static libraries at compile time -export RUSTFLAGS=--print=native-static-libs - -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 4e2a4387..78e0bbfd 100644 --- a/inst/templates/Makevars.win +++ b/inst/templates/Makevars.win.in @@ -5,9 +5,6 @@ LIBDIR = $(TARGET_DIR)/$(TARGET)/release STATLIB = $(LIBDIR)/lib{{{lib_name}}}.a PKG_LIBS = -L$(LIBDIR) -l{{{lib_name}}} -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll -# Print linked static libraries at compile time -export RUSTFLAGS=--print=native-static-libs - all: C_clean $(SHLIB): $(STATLIB) @@ -24,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 0f0e40aa..00000000 --- a/inst/templates/cran/Makevars +++ /dev/null @@ -1,39 +0,0 @@ -TARGET_DIR = ./rust/target -LIBDIR = $(TARGET_DIR)/release -STATLIB = $(LIBDIR)/lib{{{lib_name}}}.a -PKG_LIBS = -L$(LIBDIR) -l{{{lib_name}}} - -# Print linked static libraries at compile time -export RUSTFLAGS=--print=native-static-libs - -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 72f7479d..00000000 --- a/inst/templates/cran/Makevars.win +++ /dev/null @@ -1,51 +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 - -# Print linked static libraries at compile time -export RUSTFLAGS=--print=native-static-libs - -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/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/vendor_pkgs.Rd b/man/vendor_pkgs.Rd new file mode 100644 index 00000000..98e75d57 --- /dev/null +++ b/man/vendor_pkgs.Rd @@ -0,0 +1,37 @@ +% 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/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/use_cran_defaults.md b/tests/testthat/_snaps/use_cran_defaults.md deleted file mode 100644 index 4c637a16..00000000 --- a/tests/testthat/_snaps/use_cran_defaults.md +++ /dev/null @@ -1,297 +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 "*.*.*" 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 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. - ---- - - Code - use_cran_defaults() - Message - > 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 - - # Print linked static libraries at compile time - export RUSTFLAGS=--print=native-static-libs - - 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 - - # Print linked static libraries at compile time - export RUSTFLAGS=--print=native-static-libs - - 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 bb281aff..6d53afcf 100644 --- a/tests/testthat/_snaps/use_extendr.md +++ b/tests/testthat/_snaps/use_extendr.md @@ -1,27 +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 "*.*.*" 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 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. + 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)) --- @@ -44,46 +203,82 @@ --- 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 STATLIB = $(LIBDIR)/libtestpkg.a PKG_LIBS = -L$(LIBDIR) -ltestpkg - # Print linked static libraries at compile time - export RUSTFLAGS=--print=native-static-libs - 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): - # 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", "Makevars.win") + 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.in") Output TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu @@ -92,9 +287,6 @@ STATLIB = $(LIBDIR)/libtestpkg.a PKG_LIBS = -L$(LIBDIR) -ltestpkg -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll - # Print linked static libraries at compile time - export RUSTFLAGS=--print=native-static-libs - all: C_clean $(SHLIB): $(STATLIB) @@ -111,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 + # 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 \ - 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; \ + 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) @@ -140,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 @@ -213,8 +399,8 @@ 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. @@ -233,8 +419,8 @@ 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' @@ -268,39 +454,50 @@ # 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 STATLIB = $(LIBDIR)/libbar.a PKG_LIBS = -L$(LIBDIR) -lbar - # Print linked static libraries at compile time - export RUSTFLAGS=--print=native-static-libs - 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): - # 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/test-cran-compliance.R b/tests/testthat/test-cran-compliance.R new file mode 100644 index 00000000..1eac8c7d --- /dev/null +++ b/tests/testthat/test-cran-compliance.R @@ -0,0 +1,44 @@ +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) + + 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-use_cran_defaults.R b/tests/testthat/test-use_cran_defaults.R deleted file mode 100644 index 5b3db4b7..00000000 --- a/tests/testthat/test-use_cran_defaults.R +++ /dev/null @@ -1,41 +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(), transform = mask_any_version) - 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", { - 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_any_version) - expect_true(file.exists(file.path("src", "rust", "vendor.tar.xz"))) -}) diff --git a/tests/testthat/test-use_extendr.R b/tests/testthat/test-use_extendr.R index 68c6571d..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(), transform = mask_any_version) + 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/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.