Skip to content

Commit

Permalink
Adds CRAN compliance helpers and vignette (#320)
Browse files Browse the repository at this point in the history
* add use_cran_defaults() function along with vendor_pkgs()

* add articles to build ignore

* lint

* address testthat snaps being different based on OS and cargo path. Now has a snapshot for the invisibly returned data frame instead. This should not change (unless order is not preserved)

* address lintr

* resolve pkgdown CI

* address CI lintr

* Update R/cran-compliance.R

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update R/cran-compliance.R

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update R/cran-compliance.R

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update R/cran-compliance.R

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update vignettes/articles/cran-compliance.Rmd

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update vignettes/articles/cran-compliance.Rmd

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update vignettes/articles/cran-compliance.Rmd

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update vignettes/articles/cran-compliance.Rmd

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update vignettes/articles/cran-compliance.Rmd

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update vignettes/articles/cran-compliance.Rmd

Co-authored-by: Ilia Kosenkov <[email protected]>

* Update R/cran-compliance.R

Co-authored-by: Ilia Kosenkov <[email protected]>

* Apply suggestions from code review

Co-authored-by: Ilia Kosenkov <[email protected]>

* explicitly print data frame for tests

* ensure update_res only exists when cargo_lock_fp file does not

* Fix how crate versions are detected

* Execute `{styler}` over package

* Update snapshots

* Don't use `print()` in tests

* Reorder asserts

* Strip ANSI symbols

* Ensure order

* Lintr

* Update snapshot

* Reuse callback

* update news

* add OS check for Sys.chmod add test for vendor.tar.xz existing

* update Makevars to better clean up vendored libraries

* remove superfluous new line

* update snaps

* update NEWS to reference PR

---------

Co-authored-by: Ilia Kosenkov <[email protected]>
Co-authored-by: Ilia Kosenkov <[email protected]>
  • Loading branch information
3 people authored Dec 29, 2023
1 parent 09bfeba commit edc5476
Show file tree
Hide file tree
Showing 15 changed files with 730 additions and 8 deletions.
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
^\.idea$
^inst/libgcc_mock/libgcc_eh.a$
^CRAN-SUBMISSION$
^vignettes/articles$
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
224 changes: 224 additions & 0 deletions R/cran-compliance.R
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 6 additions & 4 deletions R/features.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 3 additions & 4 deletions README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
)
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ reference:
- register_extendr
- write_license_note
- clean
- cran

- title: Various utility functions
contents:
Expand Down
36 changes: 36 additions & 0 deletions inst/templates/cran/Makevars
Original file line number Diff line number Diff line change
@@ -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)
55 changes: 55 additions & 0 deletions inst/templates/cran/Makevars.win
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit edc5476

Please sign in to comment.