
Initialize a new R project with a standard folder structure
Source:R/init-project.R
init_project.Rdinit_project() creates a new R project at the given path with an
opinionated folder structure suited for research workflows. It optionally
initializes renv for package management and git for version control, and
records the structure it resolved in a project config, _toolero.yml, at
the project root.
Usage
init_project(
path,
use_renv = TRUE,
use_git = TRUE,
custom_folders = NULL,
config = NULL,
open = FALSE,
branding = "none",
uw_branding = deprecated(),
use_readme = TRUE,
use_rprofile = FALSE,
scaffold_fn = renv::scaffold
)Arguments
- path
A character string with the path and name of the new project (e.g.,
"~/Documents/my-project").- use_renv
Logical. If
TRUE, initializesrenvin the new project. Defaults toTRUE.- use_git
Logical. If
TRUE, initializes a git repository in the new project. Defaults toTRUE.- custom_folders
A character vector of folder names to add to or remove from the project structure after the base set is resolved. Bare names (e.g.,
"models") add a folder. Names prefixed with"-"(e.g.,"-output/figures") suppress creation of that folder. When removing from the built-in default set, only the named leaf is suppressed – parent directories are preserved, so"-output/figures"still leaves anoutput/folder behind. When removing from a set supplied viaconfig, parents are not preserved: a config is an explicit and complete statement of the structure, so nothing is added back that the author did not ask for. Duplicates of existing folders generate a message and are skipped. References to non-existent folders via"-"generate a warning."-R"is the one removal that does not take effect on disk: seeconfigbelow. Defaults toNULL.- config
A character string. Path to a YAML project config file produced by
generate_project_config(). When supplied, the folder list in the config replaces the built-in standard structure entirely, and anyconventions:it declares override the defaults key by key.custom_foldersis still applied on top of the config-derived set. Defaults toNULL.One folder cannot be suppressed, by a config or by
custom_folders:R/.usethis::create_project()creates it unconditionally, so it is present in every projectinit_project()makes. A structure that leaves it out is honored everywhere else –R/is absent from the project config, gets no.gitkeep, and is not audited bycheck_project()– but the directory itself is there.- open
Logical. If
TRUE, opens the new project in RStudio after creation. Defaults toFALSE.- branding
Character or logical. Controls whether an
assets/folder is created and populated.TRUEpopulates it with generic placeholder branding files (logo.png,favicon.png,header.html,footer.html,styles.css)."uw-madison"populates it with UW-Madison RCI branding files under the same standardized names."none"orFALSEcreates noassets/folder. Defaults to"none". When branding is enabled,assets/joins the project's folder set and is recorded in the project config alongside every other folder, so downstream packages can find the branding files without being told about them separately. Note thatfavicon.pngis included in the asset set but is not automatically wired into Quarto output – favicons are a website-project option set in_quarto.ymlrather than a per-document HTML option.- uw_branding
Use
brandinginstead.uw_branding = TRUEnow maps tobranding = "uw-madison";uw_branding = FALSEmaps tobranding = "none".- use_readme
Logical or character. Controls whether a README file is created at the project root.
TRUEcreatesREADME.mdfrom the generalist toolero template.FALSEcreates no README file."plain"createsREADME.txtwith the same generalist content asREADME.md– only the extension differs, not the content. Defaults toTRUE.- use_rprofile
Logical or character. Controls whether the project's
.Rprofilealso sources a personal one, so a project underrenvdoes not silently shadow the user's own startup customizations.TRUEappends a guarded block that sources~/.Rprofileif it exists. A character string names a specific file to source instead – useful for a personal profile kept somewhere other than the platform default, such as one tracked in a dotfiles repository (e.g.use_rprofile = "~/dotfiles/rprofile").FALSEadds nothing. If the named file does not exist yet, a warning is issued wheninit_project()runs, but the guard is still written – see the "Personal.Rprofileand renv" section below. Defaults toFALSE.- scaffold_fn
A function. Called as
scaffold_fn(project = path)whenuse_renv = TRUE, in place of callingrenv::scaffold()directly. Defaults torenv::scaffold. Exists for the same reasondetect_execution_context()'sinteractive_fndoes: so tests can substitute a fake and exerciseuse_renv = TRUEwithout loading renv's namespace into the test process. Overriding it outside of tests is unsupported.
The project config
init_project() writes _toolero.yml to the project root, recording the
folder set it resolved and the naming conventions in force. The file
records the resolved structure, never the inputs that produced it, so a
project built from a config, one built with custom_folders, and one
built from the defaults all produce the same shape of file and a reader
never has to replay anything to learn what the project looks like.
It exists because the structure is configurable. check_project() can
audit a customized project without being handed the same config again, and
containr and submitr can resolve where code, data, and outputs live
rather than assuming. Commit the file: it describes the project, not the
machine it was created on.
The format is experimental and may gain keys before it settles. The
schema_version field exists so that a reader can tell whether it
understands what it is holding.
The .here marker
init_project() also writes an empty .here file at the project root.
It marks the root for here::here(), which the documents
create_qmd() scaffolds use to build paths, and for toolero's own
functions that take paths from the project root (resolve_input_path(),
save_output(), generate_manifest()). An .Rproj file or a .git
folder would serve too, but a project created outside RStudio and
without git has neither, so .here guarantees a marker. Commit it. It
is never sent to an execute node, so there the job's scratch directory
stands in for the root. An existing .here is left as it is.
Empty folders and git
Each folder init_project() creates that is still empty when the call
finishes receives a zero-byte .gitkeep.
git tracks files rather than directories, so without this a scaffolded
structure survives nothing: the opening commit contains the files at the
project root and none of the layout, and a collaborator cloning the
repository gets a project with no folders in it. The placeholders are
written whether or not use_git = TRUE, since a project can be
git-initialized at any point afterwards.
Folders that already have content are left alone – assets/ holds
branding files by then and is tracked on the strength of those.
The active project
init_project() makes the new project the active usethis project for the
duration of the call, and restores whichever project was active before when
it returns. Nothing is left pointing somewhere the caller did not ask for.
This matters more than it sounds. usethis::create_project() sets the
active project only for its own duration – it uses
usethis::local_project() internally and restores the caller's project on
exit when open = FALSE. Any step that resolves paths through the active
project therefore has to set it again explicitly. In v0.5.0 and earlier
init_project() did not, so with use_git = TRUE the git initialization
and its opening commit ran against whatever project happened to be active
in the calling session rather than the project just created.
Dependency discovery and renv
When use_renv = TRUE, init_project() calls renv::scaffold(), which
creates renv/library, renv/activate.R, renv/.gitignore, an
.Rprofile that activates the project in future sessions, and an initial
renv.lock.
The call is made through the injectable scaffold_fn argument rather
than by name, purely so tests can substitute a stand-in and never load
renv's namespace into the test process. Loading it there is what broke
covr::package_coverage(): renv's load hook takes over .libPaths(),
and every package a test reaches with :: afterwards (rather than one
already loaded) becomes unresolvable. Two environment variables papered
over the symptom before this argument existed; see
covr-renv-incident.md for the incident this closes out.
Three things changed here in v0.5.0, and they are worth understanding together.
renv::scaffold() replaces renv::init(). init() loads the new project
into the calling session, repointing .libPaths() at a library that is
empty apart from renv itself – so every package the caller had
available vanishes until they restart R. Its restart argument suppresses
the restart, not the activation. That is reasonable behavior for someone
adopting renv in the project they are sitting in, and the wrong behavior
for a function whose job is to scaffold a project somewhere else.
scaffold() builds the same infrastructure and leaves the caller's
session untouched.
The .renvignore containing *.qmd is gone. It excluded Quarto documents
from renv's dependency discovery, which meant that a project whose
library() calls live in its .qmd source – the arrangement this
package recommends – could snapshot a lockfile with none of the analysis
packages in it, and containr::generate_dockerfile() would then build an
image that could not run the analysis. At the point the file was written
the project contained no .qmd files at all, so it never affected the
snapshot taken at creation time; its only effect was on every snapshot the
user took afterwards.
The snapshot at creation time is gone too, for the same underlying reason
the .renvignore was pointless there: a project that has just been
created has no code in it, so there is nothing to discover and nothing
worth recording. Take a snapshot yourself once the project has code, and
before containerizing:
renv::snapshot()Personal .Rprofile and renv
R reads exactly one .Rprofile per session: the project's own if the
working directory has one, a personal one only if it does not. When
use_renv = TRUE, renv::scaffold() writes a project .Rprofile
containing source("renv/activate.R"), and from that point on a user's
own startup customizations – aliases, options, personal helper functions
– are shadowed for every session opened in this project. Nothing warns
about this; it simply stops loading.
use_rprofile = TRUE appends a guarded block to the project's
.Rprofile, after renv's own activation line, that sources
~/.Rprofile if it exists. Pass a character string instead of TRUE to
source a different file – for instance a profile tracked in a dotfiles
repository rather than kept at the platform default location. Either
way, the existence check happens inside the written block, at every
session start, rather than being baked in once, so a file written or
edited after the project is created is still picked up. If the named
file does not exist yet when init_project() itself runs, a warning
says so – most often a sign of a typo – but the guard is written
regardless, since the file may simply not exist yet.
R has no mechanism for merging profiles, so the two files stack rather
than combine: the personal file runs in the same session, top to bottom,
at the point where the block appears. Settings that do not overlap
coexist without trouble. If both files set the same option (repos, for
example), the later line wins, and because the block is appended at the
end of the project .Rprofile, the personal value overrides anything set
above it, including values renv established during activation. A
personal profile that must not touch such options inside a project can
guard them, for instance with if (!file.exists("renv.lock")).
The personal file runs under renv's restricted library paths. Packages
it attaches or relies on – devtools, or a clipboard helper that uses
clipr – are found only if they are installed in the project library,
so a startup line such as require(devtools) may warn, and a helper
that depends on an optional package should fall back gracefully when
that package is absent.
Defaults to FALSE because it cuts against renv's own isolation goal: a
project that automatically re-sources a personal environment is no
longer fully isolated from it. The TRUE mode is also written
generically – the block sources whichever file sits at ~/.Rprofile
for whoever opens the project, not a specific person's file – so a
collaborator who clones the project gets the same behavior the
project's original author chose, rather than one tied to a specific
person's home directory. A character path is written into the guard
exactly as given, so the same portability holds as long as the path
itself is written portably (~-relative, say, rather than an absolute
path tied to one machine).
Examples
if (FALSE) { # \dontrun{
init_project(path = file.path(tempdir(), "project1"),
use_renv = FALSE, use_git = FALSE)
# Generic placeholder branding
init_project(path = file.path(tempdir(), "project2"),
branding = TRUE, use_renv = FALSE, use_git = FALSE)
# UW-Madison RCI branding
init_project(path = file.path(tempdir(), "project2b"),
branding = "uw-madison", use_renv = FALSE, use_git = FALSE)
# Add a folder and suppress one from the standard set
init_project(path = file.path(tempdir(), "project3"),
custom_folders = c("models", "-output/figures"),
use_renv = FALSE, use_git = FALSE)
# Drive structure entirely from a config file
init_project(path = file.path(tempdir(), "project4"),
config = "~/linguistics-project.yml",
use_renv = FALSE, use_git = FALSE)
# Plain-text README instead of Markdown (same content, README.txt)
init_project(path = file.path(tempdir(), "project5"),
use_readme = "plain", use_renv = FALSE, use_git = FALSE)
# Skip the README entirely
init_project(path = file.path(tempdir(), "project6"),
use_readme = FALSE, use_renv = FALSE, use_git = FALSE)
# Keep loading your own ~/.Rprofile customizations under renv (the
# scenario this argument exists for -- renv's own generated .Rprofile
# would otherwise shadow ~/.Rprofile entirely)
init_project(path = file.path(tempdir(), "project7"),
use_rprofile = TRUE, use_git = FALSE)
# Source a personal profile kept somewhere other than ~/.Rprofile
init_project(path = file.path(tempdir(), "project8"),
use_rprofile = "~/dotfiles/rprofile", use_git = FALSE)
} # }