Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Development version

## Inline scripts

* `rig run` now support self-contained scripts, that declare the packages
and the R version they need in a `# /// script` comment block. rig then
runs the script in its own environment, in the cache directory, and
Expand All @@ -16,6 +18,12 @@
solve the dependencies again, e.g. after `rig proj add`, as long as they
still fit `rproj.toml`. Use `--upgrade` to pick the latest versions.

* `rig proj init`, `rig proj add` and `rig proj remove` have a new
`--script` option, to create and edit the `# /// script` block of an R
script, instead of a project's `rproj.toml` (#414).

## Other new features

* `rig add` now has a `--json` option. It prints information about the
installed R version (or the already installed version that was kept),
including whether it is a new installation and whether it is the default.
Expand Down
46 changes: 46 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,14 @@ pub fn rig_app() -> Command {
.short('r')
.num_args(1)
.required(false),
)
.arg(
Arg::new("script")
.help("Create the inline metadata block of this R script, instead of a project")
.long("script")
.value_name("SCRIPT")
.num_args(1)
.required(false),
),
)
.subcommand(
Expand Down Expand Up @@ -1602,6 +1610,15 @@ pub fn rig_app() -> Command {
.help("Add as a dev (development) dependency")
.long("dev")
.num_args(0)
.required(false)
.conflicts_with("script"),
)
.arg(
Arg::new("script")
.help("Add the packages to the inline metadata of this R script, instead of rproj.toml")
.long("script")
.value_name("SCRIPT")
.num_args(1)
.required(false),
)
.arg(
Expand Down Expand Up @@ -1633,6 +1650,14 @@ pub fn rig_app() -> Command {
.required(true)
.num_args(1..),
)
.arg(
Arg::new("script")
.help("Remove the packages from the inline metadata of this R script, instead of rproj.toml")
.long("script")
.value_name("SCRIPT")
.num_args(1)
.required(false),
)
.arg(
Arg::new("no-lock")
.help("Only update rproj.toml, do not update rproj.lock")
Expand Down Expand Up @@ -3314,6 +3339,27 @@ mod tests {
assert_eq!(sub.get_one::<String>("platform").unwrap(), "macos-arm64");
}

#[test]
fn proj_init_add_remove_take_a_script() {
for cmd in [
vec!["rig", "proj", "init", "--script", "a.R"],
vec!["rig", "proj", "add", "--script", "a.R", "cli"],
vec!["rig", "proj", "remove", "--script", "a.R", "cli"],
] {
let matches = rig_app().try_get_matches_from(&cmd).unwrap();
let (_name, sub) = matches.subcommand().unwrap();
let (_name, sub) = sub.subcommand().unwrap();
assert_eq!(
sub.get_one::<String>("script").map(String::as_str),
Some("a.R")
);
}
// script blocks have no dependency groups
assert!(rig_app()
.try_get_matches_from(["rig", "proj", "add", "--script", "a.R", "--dev", "cli"])
.is_err());
}

#[test]
fn proj_sync_frozen_flag() {
let matches = rig_app()
Expand Down
8 changes: 4 additions & 4 deletions src/help-generated.in

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/help/proj-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ ones after it: the leftmost non-zero component for `^` (`^0.2.3` is `>= 0.2.3, <
0.3.0`), the second component for `~`. R versions can have any number of
components, so `^1.1.0.9000` is `>= 1.1.0.9000, < 2.0.0.0`.

## Scripts

`--script script.R` adds the packages to the `# /// script` block of an R
script, instead of `rproj.toml`, see [scripts with inline dependencies](run.qmd#scripts-with-inline-dependencies). It
creates the block if the script has none. The packages are given the same way as
for a project, and a local path is recorded relative to the script's directory.
A script has no dependency groups, so `--script` does not work with `--dev`.

Then rig sets up the script's environment in its cache directory, the same way
`rig run script.R` would, so the next `rig run` can start right away.
`--no-sync` and `--no-lock` work the same way as for a project. If resolving the
dependencies fails, the script is restored to what it was.

## Options

`--dev` adds the packages as development dependencies, into the
Expand Down
19 changes: 19 additions & 0 deletions src/help/proj-init.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ deleted and rebuilt at any time.
Note that `R --vanilla` ignores `.Renviron`, and so does not use the project
library.

## Scripts

`rig proj init --script script.R` sets up a single script instead of a project:
it adds a `# /// script` block with an R requirement to the top of the script,
or creates the script, if it does not exist yet:

```r
# /// script
# [dependencies]
# R = ">= 4.6"
# ///
```

The block goes after a `#!` line, if the script has one. It does not create any
other file. Add packages to the block with [`rig proj add --script`](#rig-proj-add), and run the
script with [`rig run`](run.qmd), which sets up an environment for it, see [scripts with
inline dependencies](run.qmd#scripts-with-inline-dependencies). rig refuses to replace a block that is already there, pass
`--force` to replace it.

## Options

`--r-version` sets the R version the project is for. It does not have to be
Expand Down
6 changes: 6 additions & 0 deletions src/help/proj-remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Naming a package that is not a dependency in `rproj.toml` is an error, and
none of the named packages are removed if any of them is not found, so a
typo cannot silently remove the wrong set of packages.

## Scripts

`--script script.R` removes the packages from the `# /// script` block of an R
script, instead of `rproj.toml`, see [scripts with inline dependencies](run.qmd#scripts-with-inline-dependencies). Then rig
sets up the script's new environment, the same way [`rig proj add --script`](#rig-proj-add) does.

## Options

`--no-sync` updates `rproj.toml` and `rproj.lock`, but does not touch the project
Expand Down
5 changes: 5 additions & 0 deletions src/help/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ share one environment, and changing the block creates a new one. A script
with a block always uses its own environment, even inside a project.
`--r-version` selects the R version, which must fit the `R` requirement.

`rig proj init --script script.R` adds a block with an `R` requirement to a
script (or creates the script), and `rig proj add --script script.R <package>`
and `rig proj remove --script script.R <package>` edit its `[dependencies]`, so
you do not have to write the block by hand.

Like a project's lock file, the environment keeps the package versions it
was created with. Use `--upgrade` to solve the dependencies again, with the
latest versions that fit, or `--upgrade-package` to upgrade only some
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ mod rproj;
mod run;
mod rvenv;
mod rversion;
mod script_meta;
mod solver;
mod stdout_redirect;
mod textfmt;
mod utils;
use common::*;
Expand Down
81 changes: 52 additions & 29 deletions src/proj.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ fn sc_proj_init(
_projargs: &ArgMatches,
_mainargs: &ArgMatches,
) -> Result<(), Box<dyn Error>> {
if let Some(script) = args.get_one::<String>("script") {
return crate::script_meta::sc_proj_init_script(Path::new(script), args);
}
let root = std::env::current_dir()?;
let force = args.get_flag("force");

Expand Down Expand Up @@ -205,7 +208,7 @@ pub fn init_rvenv_for_manifest(
/// what the project is set up for should be the R it will be locked and synced
/// against, see [`proj_lock_r_version`]. Nothing in the committed `.rvenv`
/// layout is tied to an R version.
fn resolve_project_r_version(args: &ArgMatches) -> Result<String, Box<dyn Error>> {
pub(crate) fn resolve_project_r_version(args: &ArgMatches) -> Result<String, Box<dyn Error>> {
if let Some(rv) = args.get_one::<String>("r-version") {
return Ok(rv.to_string());
}
Expand Down Expand Up @@ -676,13 +679,13 @@ fn write_description_to(path: &Path, manifest: &Rproj) -> Result<(), Box<dyn Err
/// `<package>@<version>`, or a git/GitHub reference, already fetched to learn
/// its real package name (from `DESCRIPTION`'s `Package:` field, which may
/// differ from the repository name) and pinned commit.
enum AddSpec {
pub(crate) enum AddSpec {
Cran(String, String),
Remote(String, Box<DepTable>),
}

impl AddSpec {
fn name(&self) -> &str {
pub(crate) fn name(&self) -> &str {
match self {
AddSpec::Cran(name, _) => name,
AddSpec::Remote(name, _) => name,
Expand All @@ -695,7 +698,7 @@ impl AddSpec {
/// is written to `rproj.toml` -- see [`fetch_and_read_git_package`]. `root`
/// is the project's own directory, against which a local path is made
/// relative -- see [`relativize_to_root`].
fn parse_add_arg(spec: &str, root: &Path) -> Result<AddSpec, Box<dyn Error>> {
pub(crate) fn parse_add_arg(spec: &str, root: &Path) -> Result<AddSpec, Box<dyn Error>> {
match crate::pkgsource::parse_pkg_source(spec)? {
crate::pkgsource::PkgSource::Cran => {
let (name, version) = parse_add_spec(spec)?;
Expand Down Expand Up @@ -741,13 +744,49 @@ fn parse_add_arg(spec: &str, root: &Path) -> Result<AddSpec, Box<dyn Error>> {
}
}

/// Add the package of `spec` to `manifest`, see [`Rproj::add_dependency`]
/// and [`Rproj::add_remote_dependency`], and return the message that tells
/// the user what happened to `target`, the file that lists the dependencies.
pub(crate) fn add_spec_to_manifest(
manifest: &mut Rproj,
spec: &AddSpec,
dev: bool,
target: &str,
) -> String {
match spec {
AddSpec::Cran(name, version) => match manifest.add_dependency(name, version, dev) {
Some(previous) if previous == *version => {
format!("Kept {} ({}) in {}", name, version, target)
}
Some(previous) => format!(
"Updated {} in {}, {} -> {}",
name, target, previous, version
),
None => format!("Added {} ({}) to {}", name, version, target),
},
AddSpec::Remote(name, table) => {
manifest.add_remote_dependency(name, (**table).clone(), dev);
let source = table
.git
.as_deref()
.or(table.url.as_deref())
.or(table.path.as_deref())
.unwrap_or_default();
format!("Added {} ({}) to {}", name, source, target)
}
}
}

/// Add dependencies to `rproj.toml`, then update the lockfile and install
/// them: `rig proj add`.
fn sc_proj_add(
args: &ArgMatches,
_projargs: &ArgMatches,
_mainargs: &ArgMatches,
) -> Result<(), Box<dyn Error>> {
if let Some(script) = args.get_one::<String>("script") {
return crate::script_meta::sc_proj_add_script(Path::new(script), args);
}
// The project is the nearest one at or above the current directory, like
// `rig proj sync`, so that `rig proj add` works from a subdirectory.
let cwd = std::env::current_dir()?;
Expand Down Expand Up @@ -795,31 +834,12 @@ fn sc_proj_add(
));
}

messages.push(match spec {
AddSpec::Cran(name, version) => {
let previous = manifest.add_dependency(name, version, dev);
match previous {
Some(previous) if previous == *version => {
format!("Kept {} ({}) in {}", name, version, RPROJ_MANIFEST_FILE)
}
Some(previous) => format!(
"Updated {} in {}, {} -> {}",
name, RPROJ_MANIFEST_FILE, previous, version
),
None => format!("Added {} ({}) to {}", name, version, RPROJ_MANIFEST_FILE),
}
}
AddSpec::Remote(name, table) => {
manifest.add_remote_dependency(name, (**table).clone(), dev);
let source = table
.git
.as_deref()
.or(table.url.as_deref())
.or(table.path.as_deref())
.unwrap_or_default();
format!("Added {} ({}) to {}", name, source, RPROJ_MANIFEST_FILE)
}
});
messages.push(add_spec_to_manifest(
&mut manifest,
spec,
dev,
RPROJ_MANIFEST_FILE,
));

if let Some(doc) = original_doc.as_mut() {
let path: &[&str] = if dev {
Expand Down Expand Up @@ -886,6 +906,9 @@ fn sc_proj_remove(
_projargs: &ArgMatches,
_mainargs: &ArgMatches,
) -> Result<(), Box<dyn Error>> {
if let Some(script) = args.get_one::<String>("script") {
return crate::script_meta::sc_proj_remove_script(Path::new(script), args);
}
let cwd = std::env::current_dir()?;
let root = find_project_root(&cwd).unwrap_or(cwd);
let path = root.join(RPROJ_MANIFEST_FILE);
Expand Down
Loading
Loading