diff --git a/.claude/skills/refresh-patch-stack/SKILL.md b/.claude/skills/refresh-patch-stack/SKILL.md new file mode 100644 index 00000000..7402cb0a --- /dev/null +++ b/.claude/skills/refresh-patch-stack/SKILL.md @@ -0,0 +1,164 @@ +--- +name: refresh-patch-stack +description: Refresh this fork's patch stack against upstream r-lib/cpp11 — merge the mirrored `main` into every patch branch, resolve conflicts, retire patches upstream has superseded, and rebuild `fork`. Use when asked to refresh, sync, or update the patch stack, or when a scheduled routine fires for it. +--- + +# Refresh the patch stack + +This fork is a patch stack on top of +[`r-lib/cpp11`](https://github.com/r-lib/cpp11). +This skill brings it up to date with upstream. + +Read [`.github/PATCHSTACK.md`](../../../.github/PATCHSTACK.md) first +if you are not already familiar with the branch layout. + +## Layout + +- `main` is a 1:1 mirror of upstream, hard-reset by the Pull app. + Never commit to it, and never merge into it. +- `a-*`, `b-*`, `f-*` are the patch branches, + one logical change each, based on `main`. +- `fork` is the default branch: + `main` plus every patch, squashed in lexicographic branch order. + It is rebuilt from scratch here and must never be merged into by hand. +- A long-lived draft pull request from `fork` into `main` provides CI. + Do not close it; + pushing `fork` is what re-runs the checks. + +## When there is nothing to do + +Fetch first, then compare: + +```bash +git fetch --prune origin +git fetch --prune upstream # https://github.com/r-lib/cpp11.git +git rev-parse origin/main upstream/main +``` + +If `origin/main` has not moved since `patchstack/upstream-base`, +and no patch branch has changed, +stop and report that the stack is current. +Do not rebuild `fork` just to produce a commit. + +If `origin/main` still lags `upstream/main`, +the Pull app has not run yet. +Say so and stop: +mirroring is the app's job, not this skill's. + +## Refreshing a patch branch + +For each `a-*`, `b-*`, `f-*` branch, **merge** `origin/main` into it. +Merge rather than rebase: +the merge commit is where the conflict resolution is recorded, +and it is what the next refresh builds on. + +```bash +git checkout -B "$branch" "origin/$branch" +git merge --no-ff origin/main +``` + +A clean merge that leaves the branch's net effect unchanged +needs no further thought — push it. + +### When it conflicts + +Resolve toward **the fork's intent on upstream's new shape**. +Upstream reformats (Air for R, clang-format for C++) +and restructures; +re-apply the patch on top of that rather than reverting it. + +Never resolve a conflict by taking the fork's whole file back. +That silently drops upstream's work in the same file. + +### When a patch has been superseded + +Upstream sometimes implements a patch's feature itself, +or fixes the bug it worked around. +Then the resolution **is the removal of the feature**: +take upstream's version of every file the patch touched, +so the branch's tree ends up equal to `origin/main`'s. + +Say so explicitly in the merge commit message, +and say *why* — which upstream pull request or issue supersedes it, +and how you established that. +A patch retired without that reasoning +is indistinguishable from one dropped by accident. + +Two signals worth checking before concluding a patch still earns its place: + +- the upstream pull request that carries it, if there is one, + may have been closed; +- upstream's `NEWS.md` may cite the issue the patch addresses. + +An empty branch is not deleted here. +Leave it; +the next patchstack sync detects it, deletes it, +and records the disposition in `refs/notes/patchstack`. + +## Verifying + +Before pushing anything, from the rebuilt `fork`: + +```bash +air format . && git diff --exit-code +clang-format --dry-run -Werror $(git diff --name-only origin/main -- '*.hpp' '*.cpp') +Rscript -e 'devtools::load_all(quiet = TRUE); testthat::test_dir("tests/testthat", reporter = "summary")' +Rscript -e 'devtools::install(quiet = TRUE, upgrade = FALSE)' +Rscript -e 'devtools::clean_dll("./cpp11test"); devtools::test("./cpp11test", reporter = "summary")' +``` + +The C++ suite takes several minutes and is the one that matters most: +most of the stack is header changes, +and the R suite does not compile them. + +Report a failure rather than working around it. +A red suite means the refresh stops and a human looks at it; +it does not mean the offending patch gets dropped. + +## Rebuilding `fork` + +`fork` is `main` plus each patch's net change, +applied in lexicographic branch order: + +```bash +git checkout -B fork origin/main +git diff origin/main "origin/$branch" | git apply --index - # per branch, in order +git commit -m "" +``` + +Skip a branch whose net change against `origin/main` is empty. + +The patches are expected to commute — +each applies to `main` on its own, +and the order changes nothing. +If applying one fails, +two patches have started to overlap: +report that rather than forcing it through, +because the squashed result would then depend on ordering +and stop being reproducible. + +## Publishing + +Push the patch branches and `fork` together: + +```bash +git push --atomic --force-with-lease origin fork +``` + +`--atomic` so a stale lease leaves every branch as it was +rather than half a stack. +`--force-with-lease` because `fork` is rebuilt +and a refreshed patch branch may be rewritten. + +## Reporting + +Say, briefly: + +- which upstream commits arrived; +- for each patch: clean, resolved (and how), superseded (and why), or failed; +- the verification results; +- anything a human needs to decide. + +If every patch merged cleanly and the suites passed, +that is a short report. +Keep it short. diff --git a/.github/PATCHSTACK.md b/.github/PATCHSTACK.md new file mode 100644 index 00000000..21c19252 --- /dev/null +++ b/.github/PATCHSTACK.md @@ -0,0 +1,96 @@ +# The patch stack + +This is a fork of [r-lib/cpp11](https://github.com/r-lib/cpp11) that carries a +handful of changes upstream does not ship. +It is maintained as a *patch stack*: +one branch per change, +each a single commit on top of upstream, +and one branch that is upstream plus all of them. + +## The branches + +- **`main`** is a 1:1 mirror of `r-lib/cpp11`'s `main`. + The [Pull app](https://pull.git.ci/) hard-resets it on every sync + ([`pull.yml`](pull.yml)), + so a commit made here is discarded the next time upstream moves. + Never commit to it. +- **`fork`** is the default branch and the one to install: + `main` plus every patch, squashed in lexicographic branch order. + It is rebuilt from scratch on each sync — never commit to it either, + and never merge into it. +- **`a-fork-infra`**, **`b-*`**, **`f-*`** are the patch branches, + each based on `main`. + `a-` is this fork's own infrastructure (it sorts first, so it lands first), + `b-` is a bug fix, `f-` is a feature. + A working branch under any other name is left alone. + +## CI + +A long-lived **draft pull request from `fork` into `main`** runs the checks. +Upstream's `R-CMD-check` triggers on `pull_request` against `main`, +so the stack is checked without this fork editing a workflow file — +which also keeps `.github/workflows/` identical to upstream's +and out of the rename surface. + +Do not close that pull request and do not merge it. +Merging would put the stack on `main`, +and the Pull app would discard it on the next mirror. +Pushing `fork` is what re-runs the checks. + +## The refresh + +[`.claude/skills/refresh-patch-stack/`](/.claude/skills/refresh-patch-stack/SKILL.md) +is a Claude skill that merges the mirrored `main` into every patch branch, +resolves the conflicts, retires patches upstream has superseded, +rebuilds `fork`, and pushes. +A scheduled routine invokes it; +it can also be run by hand. + +It is a skill rather than a GitHub Actions workflow for two reasons. +Conflict resolution needs judgement — +upstream reformats and restructures, +and re-applying a patch on the new shape is not a mechanical merge. +And a workflow that rebuilds `fork` would have to push +`.github/workflows/` back to the branch, +which `GITHUB_TOKEN` may not do, +so it would need a personal access token to exist at all. + +A patch that cannot be resolved is reported and left alone, +so one stuck patch never blocks the others. +A patch whose change has landed upstream ends up empty; +the branch is deleted and the disposition recorded +in `refs/notes/patchstack`. + +## Adding a patch + +```bash +git fetch origin +git switch -c f-my-change origin/main +# ... one commit ... +git push -u origin f-my-change +``` + +The next sync folds it into `fork`. +Keep it to a single commit where you can: +the stack is easier to read, +and the squash into `fork` is what everyone consumes anyway. + +Each patch is a candidate for upstream. +Opening a pull request from its branch against `r-lib/cpp11` +costs nothing here — the branch stays exactly where the sync expects it — +and a merged patch cleans itself up on the following run. + +## Installing + +```r +pak::pak("krlmlr/cpp11") +``` + +`fork` is the default branch, so this installs the whole stack. +It is also built by +[krlmlr.r-universe.dev](https://krlmlr.r-universe.dev), +which is the faster route: + +```r +install.packages("cpp11", repos = c("https://krlmlr.r-universe.dev", getOption("repos"))) +``` diff --git a/.github/pull.yml b/.github/pull.yml new file mode 100644 index 00000000..1162ef0a --- /dev/null +++ b/.github/pull.yml @@ -0,0 +1,26 @@ +# Configuration for the "Pull" GitHub App, which keeps this fork's `main` in sync +# with the upstream r-lib/cpp11 repository. +# +# Pointers: +# - App / install / manage: https://pull.git.ci/ +# - Source: https://github.com/wei/pull +# - Configuration reference: https://github.com/wei/pull/blob/master/docs/CONFIGURATIONS.md +# - Trigger a manual sync: https://pull.git.ci/process/krlmlr/cpp11 +# +# Notes: +# - This file must live on the fork's default branch (`fork`) for Pull to read it. +# - `main` is a 1:1 mirror of r-lib/cpp11's `main`, so `hardreset` is the only +# correct merge method: any commit made on it here is discarded on the next sync. +# Nothing may be committed to `main` -- the patch branches carry this fork's work. +# - `fork` has no rule on purpose: it is not a mirror but `main` plus the squashed +# patch stack, and .github/workflows/patchstack-sync.yml rebuilds it once Pull +# has moved `main` forward. +# - Deleting this file would not disable Pull: without a configuration it hard-resets +# the fork's default branch from upstream, which is exactly what `fork` must not do. + +version: "1" + +rules: + - base: main + upstream: r-lib:main + mergeMethod: hardreset diff --git a/R/register.R b/R/register.R index 68c7f585..ef1de4de 100644 --- a/R/register.R +++ b/R/register.R @@ -61,7 +61,7 @@ cpp_register <- function( funs <- get_registered_functions(all_decorations, "cpp11::register", quiet) package <- desc::desc_get("Package", file = file.path(path, "DESCRIPTION")) - package <- sub("[.]", "_", package) + package <- gsub("[.]", "_", package) cpp_functions_definitions <- generate_cpp_functions(funs, package) @@ -114,17 +114,28 @@ cpp_register <- function( ) } + pkg_types_name <- paste0(package, c("_types.h", "_types.hpp")) + pkg_types <- c( - file.path(path, "src", paste0(package, "_types.h")), - file.path(path, "src", paste0(package, "_types.hpp")), - file.path(path, "inst", "include", paste0(package, "_types.h")), - file.path(path, "inst", "include", paste0(package, "_types.hpp")) + file.path(path, "src", pkg_types_name), + file.path(path, "src", "include", pkg_types_name), + file.path(path, "inst", "include", pkg_types_name) + ) + + # `src/cpp11.cpp` is generated next to the `src/` copies and compiled from + # `src/`, so a header in `src/include/` is included through that directory. + # A package keeping its own headers private this way then needs no include + # flag for them, and none of the other two locations changes. + pkg_types_include <- c( + pkg_types_name, + file.path("include", pkg_types_name), + pkg_types_name ) pkg_types_exist <- file.exists(pkg_types) if (any(pkg_types_exist)) { extra_includes <- c( - sprintf('#include "%s"', basename(pkg_types[pkg_types_exist])), + sprintf('#include "%s"', pkg_types_include[pkg_types_exist]), extra_includes ) } diff --git a/R/vendor.R b/R/vendor.R index 1d7dc7d2..5d4b47ac 100644 --- a/R/vendor.R +++ b/R/vendor.R @@ -8,6 +8,12 @@ #' 'cpp11 version: XYZ' to the top of the files, where XYZ is the version of #' cpp11 currently installed on your machine. #' +#' Pass `subdir` to vendor somewhere else. A package that does not want the +#' headers installed can keep them under `src/`, which leaves nothing of cpp11 +#' in the installed package; the generated `src/cpp11.cpp` reaches them by a +#' path relative to `src/`, so only that package's own `PKG_CPPFLAGS` needs to +#' know where they are. +#' #' If you choose to vendor the headers you should _remove_ `LinkingTo: #' cpp11` from your DESCRIPTION. #' @@ -16,6 +22,14 @@ #' code until you run `cpp_vendor()` again. #' #' @inheritParams cpp_register +#' @param ... These dots are for future extensions and must be empty. +#' @param subdir The directory below `path` to vendor into, as a path relative +#' to `path`. Defaults to `inst/include`, which installs the headers. +#' @param date The date recorded in the `vendored on:` header of each vendored +#' file. Defaults to the current date; pass a fixed date to make vendoring +#' reproducible. +#' @param overwrite If `TRUE`, an existing vendored copy is removed first +#' instead of raising an error. #' @return The file path to the vendored code (invisibly). #' @export #' @examples @@ -30,18 +44,30 @@ #' #' # cleanup #' unlink(dir, recursive = TRUE) -cpp_vendor <- function(path = ".") { - new <- file.path(path, "inst", "include", "cpp11") +cpp_vendor <- function( + path = ".", + ..., + subdir = file.path("inst", "include"), + date = Sys.Date(), + overwrite = FALSE +) { + check_dots_empty(...) + + new <- file.path(path, subdir, "cpp11") if (dir.exists(new)) { - stop( - "'", - new, - "' already exists\n * run unlink('", - new, - "', recursive = TRUE)", - call. = FALSE - ) + if (overwrite) { + unlink(new, recursive = TRUE) + } else { + stop( + "'", + new, + "' already exists\n * run unlink('", + new, + "', recursive = TRUE)", + call. = FALSE + ) + } } dir.create(new, recursive = TRUE, showWarnings = FALSE) @@ -56,7 +82,7 @@ cpp_vendor <- function(path = ".") { cpp11_header <- sprintf( "// cpp11 version: %s\n// vendored on: %s", cpp11_version, - Sys.Date() + as.Date(date) ) files <- list.files(current, full.names = TRUE) @@ -75,3 +101,27 @@ cpp_vendor <- function(path = ".") { invisible(new) } + +# `cpp_vendor()` takes its options after `...`, so they must be named. Anything +# reaching the dots is a misspelling or a positional argument, and silently +# ignoring it would vendor to the wrong place. rlang has `check_dots_empty()`; +# cpp11 depends on nothing, so this is the base equivalent. +check_dots_empty <- function(...) { + dots <- list(...) + if (length(dots) == 0) { + return(invisible()) + } + + named <- names(dots) + named <- named[nzchar(named)] + + stop( + "`...` must be empty.\n", + if (length(named) > 0) { + paste0(" * Unexpected argument: `", named[[1]], "`") + } else { + " * Arguments after `path` must be named." + }, + call. = FALSE + ) +} diff --git a/cpp11test/src/test-external_pointer.cpp b/cpp11test/src/test-external_pointer.cpp index 897fd4f2..d8b960aa 100644 --- a/cpp11test/src/test-external_pointer.cpp +++ b/cpp11test/src/test-external_pointer.cpp @@ -36,4 +36,53 @@ context("external_pointer-C++") { uniq.reset(); expect_true(deleted == true); } + + test_that("external_pointer preserves attributes when moved (issue #308)") { + // Test move constructor + { + int* value = new int(42); + cpp11::external_pointer p(value); + + // Set an attribute on the external pointer + Rf_setAttrib(p, R_ClassSymbol, Rf_mkString("test_class")); + + // Verify attribute exists before move + SEXP class_attr = Rf_getAttrib(p, R_ClassSymbol); + expect_true(class_attr != R_NilValue); + + // Move the external pointer using move constructor + cpp11::external_pointer p_moved = std::move(p); + + // Verify attribute is preserved after move + SEXP class_attr_after = Rf_getAttrib(p_moved, R_ClassSymbol); + expect_true(class_attr_after != R_NilValue); + expect_true(strcmp(CHAR(STRING_ELT(class_attr_after, 0)), "test_class") == 0); + + // Clean up + delete p_moved.release(); + } + + // Test move assignment operator + { + int* value1 = new int(1); + cpp11::external_pointer p1(value1); + + // Set an attribute on p1 + Rf_setAttrib(p1, R_ClassSymbol, Rf_mkString("test_class")); + + // Create p2 with nullptr (no memory leak) + cpp11::external_pointer p2(nullptr); + + // Move assign p1 to p2 + p2 = std::move(p1); + + // Verify attribute is preserved after move assignment + SEXP class_attr_after = Rf_getAttrib(p2, R_ClassSymbol); + expect_true(class_attr_after != R_NilValue); + expect_true(strcmp(CHAR(STRING_ELT(class_attr_after, 0)), "test_class") == 0); + + // Clean up + delete p2.release(); + } + } } diff --git a/inst/include/cpp11/declarations.hpp b/inst/include/cpp11/declarations.hpp index ed57721f..89f24a65 100644 --- a/inst/include/cpp11/declarations.hpp +++ b/inst/include/cpp11/declarations.hpp @@ -41,7 +41,7 @@ T& unmove(T&& t) { SEXP err = R_NilValue; \ char buf[CPP11_ERROR_BUFSIZE] = ""; \ try { -#define END_CPP11 \ +#define END_CPP11_EX(RET) \ } \ catch (cpp11::unwind_exception & e) { \ err = e.token; \ @@ -57,4 +57,5 @@ T& unmove(T&& t) { } else if (err != R_NilValue) { \ R_ContinueUnwind(err); \ } \ - return R_NilValue; + return RET; +#define END_CPP11 END_CPP11_EX(R_NilValue) diff --git a/inst/include/cpp11/external_pointer.hpp b/inst/include/cpp11/external_pointer.hpp index a62134ec..61b7b50c 100644 --- a/inst/include/cpp11/external_pointer.hpp +++ b/inst/include/cpp11/external_pointer.hpp @@ -55,8 +55,9 @@ class external_pointer { external_pointer(SEXP data) : data_(valid_type(data)) {} - external_pointer(pointer p, bool use_deleter = true, bool finalize_on_exit = true) - : data_(safe[R_MakeExternalPtr]((void*)p, R_NilValue, R_NilValue)) { + external_pointer(pointer p, bool use_deleter = true, bool finalize_on_exit = true, + SEXP prot = R_NilValue) + : data_(safe[R_MakeExternalPtr]((void*)p, R_NilValue, prot)) { if (use_deleter) { R_RegisterCFinalizerEx(data_, r_deleter, static_cast(finalize_on_exit)); } @@ -66,9 +67,19 @@ class external_pointer { data_ = safe[Rf_shallow_duplicate](rhs.data_); } - external_pointer(external_pointer&& rhs) { reset(rhs.release()); } + external_pointer(external_pointer&& rhs) { + data_ = rhs.data_; + rhs.data_ = R_NilValue; + } - external_pointer& operator=(external_pointer&& rhs) noexcept { reset(rhs.release()); } + external_pointer& operator=(external_pointer&& rhs) noexcept { + // This works even if `this == &rhs` because `data_` (a `cpp11::sexp`) handles + // the underlying resource. + data_ = rhs.data_; + // Order matters: first assign, then clear the RHS. + rhs.data_ = R_NilValue; + return *this; + } external_pointer& operator=(std::nullptr_t) noexcept { reset(); }; diff --git a/inst/include/cpp11/r_vector.hpp b/inst/include/cpp11/r_vector.hpp index 317b16f1..3acde389 100644 --- a/inst/include/cpp11/r_vector.hpp +++ b/inst/include/cpp11/r_vector.hpp @@ -270,6 +270,7 @@ class r_vector : public cpp11::r_vector { public: proxy(SEXP data, const R_xlen_t index, underlying_type* const p, bool is_altrep); + proxy(const proxy&) = default; proxy& operator=(const proxy& rhs); diff --git a/inst/include/cpp11/sexp.hpp b/inst/include/cpp11/sexp.hpp index 34f7e2b8..70927c95 100644 --- a/inst/include/cpp11/sexp.hpp +++ b/inst/include/cpp11/sexp.hpp @@ -67,11 +67,20 @@ class sexp { SEXP data() const { return data_; } /// DEPRECATED: Do not use this, it will be removed soon. - operator double() const { return REAL_ELT(data_, 0); } + [[deprecated("Implicit conversion from cpp11::sexp to double is deprecated")]] + operator double() const { + return REAL_ELT(data_, 0); + } /// DEPRECATED: Do not use this, it will be removed soon. - operator size_t() const { return REAL_ELT(data_, 0); } + [[deprecated("Implicit conversion from cpp11::sexp to size_t is deprecated")]] + operator size_t() const { + return REAL_ELT(data_, 0); + } /// DEPRECATED: Do not use this, it will be removed soon. - operator bool() const { return LOGICAL_ELT(data_, 0); } + [[deprecated("Implicit conversion from cpp11::sexp to bool is deprecated")]] + operator bool() const { + return LOGICAL_ELT(data_, 0); + } }; } // namespace cpp11 diff --git a/man/cpp_vendor.Rd b/man/cpp_vendor.Rd index 857e49cf..4712a76d 100644 --- a/man/cpp_vendor.Rd +++ b/man/cpp_vendor.Rd @@ -4,10 +4,28 @@ \alias{cpp_vendor} \title{Vendor the cpp11 dependency} \usage{ -cpp_vendor(path = ".") +cpp_vendor( + path = ".", + ..., + subdir = file.path("inst", "include"), + date = Sys.Date(), + overwrite = FALSE +) } \arguments{ \item{path}{The path to the package root directory} + +\item{...}{These dots are for future extensions and must be empty.} + +\item{subdir}{The directory below \code{path} to vendor into, as a path relative +to \code{path}. Defaults to \code{inst/include}, which installs the headers.} + +\item{date}{The date recorded in the \code{vendored on:} header of each vendored +file. Defaults to the current date; pass a fixed date to make vendoring +reproducible.} + +\item{overwrite}{If \code{TRUE}, an existing vendored copy is removed first +instead of raising an error.} } \value{ The file path to the vendored code (invisibly). @@ -22,6 +40,12 @@ headers into the \code{inst/include} folder of your package and adding 'cpp11 version: XYZ' to the top of the files, where XYZ is the version of cpp11 currently installed on your machine. +Pass \code{subdir} to vendor somewhere else. A package that does not want the +headers installed can keep them under \code{src/}, which leaves nothing of cpp11 +in the installed package; the generated \code{src/cpp11.cpp} reaches them by a +path relative to \code{src/}, so only that package's own \code{PKG_CPPFLAGS} needs to +know where they are. + If you choose to vendor the headers you should \emph{remove} \code{LinkingTo: cpp11} from your DESCRIPTION. \strong{Note}: vendoring places the responsibility of updating the code on diff --git a/tests/testthat/test-register.R b/tests/testthat/test-register.R index 7f65bb8a..7e3c86b6 100644 --- a/tests/testthat/test-register.R +++ b/tests/testthat/test-register.R @@ -685,6 +685,50 @@ describe("cpp_register", { ) }) + it("includes pkg_types.h if included in src/include", { + pkg <- local_package() + p <- pkg_path(pkg) + dir.create(file.path(p, "src", "include"), recursive = TRUE) + file.copy(test_path("single.cpp"), file.path(p, "src", "single.cpp")) + writeLines( + "#include ", + file.path(p, "src", "include", "testPkg_types.h") + ) + cpp_register(p) + + expect_true( + any( + grepl( + pattern = '#include "include/testPkg_types.h"', + x = readLines(file.path(p, "src", "cpp11.cpp")), + fixed = TRUE + ) + ) + ) + }) + + it("includes pkg_types.hpp if included in src/include", { + pkg <- local_package() + p <- pkg_path(pkg) + dir.create(file.path(p, "src", "include"), recursive = TRUE) + file.copy(test_path("single.cpp"), file.path(p, "src", "single.cpp")) + writeLines( + "#include ", + file.path(p, "src", "include", "testPkg_types.hpp") + ) + cpp_register(p) + + expect_true( + any( + grepl( + pattern = '#include "include/testPkg_types.hpp"', + x = readLines(file.path(p, "src", "cpp11.cpp")), + fixed = TRUE + ) + ) + ) + }) + it("includes pkg_types.h if included in inst/include", { pkg <- local_package() p <- pkg_path(pkg) diff --git a/tests/testthat/test-vendor.R b/tests/testthat/test-vendor.R index c70def90..ab989c5f 100644 --- a/tests/testthat/test-vendor.R +++ b/tests/testthat/test-vendor.R @@ -18,6 +18,25 @@ describe("cpp_vendor", { ) }) + it("vendors into subdir", { + pkg <- local_package() + p <- pkg_path(pkg) + + cpp_vendor(p, subdir = "src") + + expect_true(file.exists(file.path(p, "src", "cpp11.hpp"))) + expect_true(file.exists(file.path(p, "src", "cpp11", "declarations.hpp"))) + expect_false(dir.exists(file.path(p, "inst", "include", "cpp11"))) + }) + + it("errors on a misnamed argument", { + pkg <- local_package() + expect_error( + cpp_vendor(pkg_path(pkg), subdirectory = "src"), + "must be empty" + ) + }) + it("vendors cpp11", { pkg <- local_package() p <- pkg_path(pkg)