From 52dc33865fe438697d0eb736a53ef9c3db7d7f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Thu, 1 Oct 2026 15:15:33 +0200 Subject: [PATCH] Implement 'rig proj lock --update-package` --- NEWS.md | 13 ++- src/args.rs | 88 ++++++++++++++++++++ src/dcf/mod.rs | 2 +- src/help-generated.in | 4 +- src/help/proj-lock.md | 36 ++++++++- src/help/run.md | 10 +++ src/pkg/install.rs | 4 +- src/proj.rs | 181 +++++++++++++++++++++++++++++++++++++++--- src/script_meta.rs | 32 +++++++- src/scrun.rs | 3 + src/solver.rs | 146 ++++++++++++++++++++++++++++++++++ tests/test-linux.sh | 11 +++ tests/test-macos.sh | 11 +++ 13 files changed, 522 insertions(+), 19 deletions(-) diff --git a/NEWS.md b/NEWS.md index 122be64e7..818e4fe09 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,7 +3,18 @@ * `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 - installs R and the packages as needed. + installs R and the packages as needed. `rig run --upgrade` and + `rig run --upgrade-package` upgrade the packages of a script's + environment. + +* New `rig proj lock --upgrade-package` (`-P`) option, to upgrade only + some packages, and keep the versions `rproj.lock` pins for the rest. It + also takes a version requirement, e.g. `-P 'cli@>= 3.6.4'`, for this + run only (#410). + +* `rig proj lock` now keeps the versions `rproj.lock` pins when it has to + 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 add` now has a `--json` option. It prints information about the installed R version (or the already installed version that was kept), diff --git a/src/args.rs b/src/args.rs index 79647ca44..172bb5fb7 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1455,6 +1455,34 @@ pub fn rig_app() -> Command { .required(false) .conflicts_with_all(["cmd", "shell"]), ) + .arg( + Arg::new("upgrade") + .help( + "Re-resolve every dependency of a script with a `# /// script`\n\ + block, instead of keeping the versions its environment has", + ) + .long("upgrade") + .short('U') + .action(clap::ArgAction::SetTrue) + .required(false) + .conflicts_with_all(["eval", "cmd", "list", "shell", "app-type"]), + ) + .arg( + Arg::new("upgrade-package") + .help( + "Upgrade only these packages in the environment of a script\n\ + with a `# /// script` block. As or\n\ + @. Comma-separated, and can be repeated.", + ) + .long("upgrade-package") + .short('P') + .value_name("PACKAGE") + .num_args(1) + .value_delimiter(',') + .action(clap::ArgAction::Append) + .required(false) + .conflicts_with_all(["upgrade", "eval", "cmd", "list", "shell", "app-type"]), + ) .arg( Arg::new("command") .help("R script, project script name, project or R CMD command to run, with parameters") @@ -1769,6 +1797,24 @@ pub fn rig_app() -> Command { .short('U') .num_args(0) .required(false), + ) + .arg( + Arg::new("upgrade-package") + .help( + "Upgrade only these packages, and keep the versions\n\ + rproj.lock pins for the rest, where they still fit.\n\ + As or @, e.g. cli or\n\ + 'cli@>= 3.6.4'. The version is not written to\n\ + rproj.toml. Comma-separated, and can be repeated.", + ) + .long("upgrade-package") + .short('P') + .value_name("PACKAGE") + .num_args(1) + .value_delimiter(',') + .action(clap::ArgAction::Append) + .required(false) + .conflicts_with("upgrade"), ), ) .subcommand( @@ -3096,6 +3142,48 @@ mod tests { assert_eq!(platforms, vec!["macos", "ubuntu-24.04"]); } + #[test] + fn proj_lock_upgrade_package_is_repeatable_and_comma_separated() { + let matches = rig_app() + .try_get_matches_from([ + "rig", + "proj", + "lock", + "-P", + "cli,glue", + "--upgrade-package", + "rlang@>= 1.1", + ]) + .unwrap(); + let (_name, sub) = matches.subcommand().unwrap(); + let (_name, sub) = sub.subcommand().unwrap(); + let pkgs: Vec<&String> = sub.get_many::("upgrade-package").unwrap().collect(); + assert_eq!(pkgs, vec!["cli", "glue", "rlang@>= 1.1"]); + } + + #[test] + fn proj_lock_upgrade_and_upgrade_package_conflict() { + assert!(rig_app() + .try_get_matches_from(["rig", "proj", "lock", "-U", "-P", "cli"]) + .is_err()); + } + + #[test] + fn run_upgrade_flags_are_for_scripts() { + let matches = rig_app() + .try_get_matches_from(["rig", "run", "-P", "cli", "script.R"]) + .unwrap(); + let (_name, sub) = matches.subcommand().unwrap(); + let pkgs: Vec<&String> = sub.get_many::("upgrade-package").unwrap().collect(); + assert_eq!(pkgs, vec!["cli"]); + assert!(rig_app() + .try_get_matches_from(["rig", "run", "-U", "script.R"]) + .is_ok()); + assert!(rig_app() + .try_get_matches_from(["rig", "run", "-U", "-e", "1"]) + .is_err()); + } + #[test] fn proj_lock_add_platform_is_repeatable_and_comma_separated() { let matches = rig_app() diff --git a/src/dcf/mod.rs b/src/dcf/mod.rs index 2c2947725..ae47f8393 100644 --- a/src/dcf/mod.rs +++ b/src/dcf/mod.rs @@ -257,7 +257,7 @@ impl std::fmt::Display for DepVersionSpec { // e.g. Depends, or it can be used for the combined dependencies of a // package -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct PackageDependencies { pub dependencies: Vec, } diff --git a/src/help-generated.in b/src/help-generated.in index 7817c60f7..5c1657c49 100644 --- a/src/help-generated.in +++ b/src/help-generated.in @@ -84,7 +84,7 @@ const HELP_PROJ_IMPORT: &str = "\u{1b}[1m\u{1b}[34mDescription:\u{1b}[39m\u{1b}[ const ABOUT_PROJ_INIT: &str = "Create a new R project"; const HELP_PROJ_INIT: &str = "\u{1b}[1m\u{1b}[34mDescription:\u{1b}[39m\u{1b}[22m\n Set up an R project in the current directory: the \u{1b}[32mrproj.toml\u{1b}[39m manifest, plus\n the part of the project's virtual environment that belongs in version\n control.\n\n \u{1b}[32mrproj.toml\u{1b}[39m is rig's project manifest. It describes the project's metadata\n and its R and package dependencies.\n\n \u{1b}[32mrig proj init\u{1b}[39m writes a minimal skeleton: a \u{1b}[32m[project]\u{1b}[39m table with the name\n (taken from the current directory) and version, and a \u{1b}[32m[dependencies]\u{1b}[39m table\n with a single R requirement, that you then fill in.\n\n Use \u{1b}[32mrig proj import\u{1b}[39m instead to set up the same project from an existing\n \u{1b}[32mDESCRIPTION\u{1b}[39m file.\n\n\u{1b}[1m\u{1b}[34mFiles:\u{1b}[39m\u{1b}[22m\n \u{1b}[32mrig proj init\u{1b}[39m creates these, and nothing else. All of them are meant to be\n committed, so that a fresh clone of the project works right away:\n\n - \u{1b}[32mrproj.toml\u{1b}[39m - the manifest.\n - \u{1b}[32m.Renviron\u{1b}[39m, \u{1b}[32m.rvenvlib/rvenv\u{1b}[39m - boilerplate to set up R's libraries for the\n project when started from the project directory, in a terminal, of from\n an editor (RStudio, Positron, VS Code).\n - \u{1b}[32m.gitignore\u{1b}[39m - to ignore \u{1b}[32m.rvenv\u{1b}[39m which contains the project library and\n configuration files, created by \u{1b}[32mrig proj sync\u{1b}[39m.\n\n The R virtual environment in \u{1b}[32m.rvenv\u{1b}[39m, including the project library\n \u{1b}[32m.rvenv/lib\u{1b}[39m, is machine-specific and is created by \u{1b}[32mrig proj sync\u{1b}[39m . It can be\n deleted and rebuilt at any time.\n\n Note that \u{1b}[32mR --vanilla\u{1b}[39m ignores \u{1b}[32m.Renviron\u{1b}[39m, and so does not use the project\n library.\n\n\u{1b}[1m\u{1b}[34mOptions:\u{1b}[39m\u{1b}[22m\n \u{1b}[32m--r-version\u{1b}[39m sets the R version the project is for. It does not have to be\n installed. Defaults to the current default R version, or the current R\n release if there is no default.\n\n rig refuses to overwrite any of the files above; pass \u{1b}[32m--force\u{1b}[39m to replace\n them. The \u{1b}[32m.gitignore\u{1b}[39m block is the exception: rig never refuses on an\n existing \u{1b}[32m.gitignore\u{1b}[39m, it just merges its block into it (or adds one),\n leaving the rest of the file alone, and \u{1b}[32m--force\u{1b}[39m does not change that."; const ABOUT_PROJ_LOCK: &str = "Resolve project dependencies and write \u{1b}[32mrproj.lock\u{1b}[39m"; -const HELP_PROJ_LOCK: &str = "\u{1b}[1m\u{1b}[34mDescription:\u{1b}[39m\u{1b}[22m\n Resolve the dependencies of an R project to a concrete set of package\n versions, and write the result to \u{1b}[32mrproj.lock\u{1b}[39m.\n\n rig reads the project manifest, \u{1b}[32mrproj.toml\u{1b}[39m, in the current directory, and\n uses its built-in solver to find a compatible set of package versions from\n the configured repositories.\n\n \u{1b}[32mrig proj solve\u{1b}[39m does not run R.\n\n Use \u{1b}[32m--r-version\u{1b}[39m to solve for a specific R version.\n\n\u{1b}[1m\u{1b}[34mOptional dependencies and dependency groups:\u{1b}[39m\u{1b}[22m\n Every \u{1b}[32m[dependency-groups.*]\u{1b}[39m table (\u{1b}[32mdev\u{1b}[39m, \u{1b}[32menhances\u{1b}[39m, or any other name)\n and every \u{1b}[32m[optional-dependencies.*]\u{1b}[39m extra are optional dependencies:\n packages the project suggests or can take advantage of, but does not need to\n run. \u{1b}[32mrig proj lock\u{1b}[39m always solves all of them together with the project's\n hard dependencies, in one solve, so \u{1b}[32mrproj.lock\u{1b}[39m is complete -- a version\n picked for a shared package is the same whether it got pulled in as a hard\n or an optional dependency, and every group and extra is available to install\n without a new solve. A \u{1b}[32m[dependency-groups.*]\u{1b}[39m table can also \u{1b}[32minclude-groups\n = [...]\u{1b}[39m other groups, pulling in their packages too; \u{1b}[32mrig proj lock\u{1b}[39m follows\n this when solving, and rejects a cycle (a group that includes itself,\n directly or through others).\n\n \u{1b}[32mrig proj sync\u{1b}[39m is where a subset of this is picked for\n installation -- by default \u{1b}[32mmain\u{1b}[39m plus the \u{1b}[32mdev\u{1b}[39m group, more with\n \u{1b}[32m--group\u{1b}[39m/\u{1b}[32m--all-groups\u{1b}[39m/\u{1b}[32m--extra\u{1b}[39m/\u{1b}[32m--all-extras\u{1b}[39m.\n\n\u{1b}[1m\u{1b}[34mThe R version:\u{1b}[39m\u{1b}[22m\n Without \u{1b}[32m--r-version\u{1b}[39m rig solves for the default R version, provided the\n manifest's own \u{1b}[32mR\u{1b}[39m requirement allows it. If it does not, rig takes the\n newest installed R version that does, and failing that the current R\n release. The version it picks does not have to be installed: \u{1b}[32mrig proj lock\u{1b}[39m\n never runs R, and \u{1b}[32mrig proj sync\u{1b}[39m installs the R version the lock file names.\n\n\u{1b}[1m\u{1b}[34mSource and binary packages:\u{1b}[39m\u{1b}[22m\n The solver considers binary packages as well as source packages, and\n prefers a binary build when one is available for the same version. By\n default a binary build never changes \u{1b}[3mwhich version\u{1b}[23m rig picks: the newest\n suitable version wins, and a binary of it is used if there is one. Use\n \u{1b}[32m--prefer-binary\u{1b}[39m to let an older version win instead, when the newest one\n has no binary but an older one does, typically because a version was\n released so recently that it has not been built yet. Only the three newest\n versions of a package are considered; \u{1b}[32m--prefer-binary=5\u{1b}[39m considers five.\n Versions held back this way are marked in the output.\n\n By default rig solves for this machine plus the three other common\n platforms (macOS arm64, Windows x86_64 and GNU Linux x86_64), and for\n source packages only, so the lock file also works on platforms without\n binary packages. \u{1b}[32mrig proj sync\u{1b}[39m only uses the source target if no other\n target matches the machine. Use\n \u{1b}[32m--platform\u{1b}[39m to solve for a different set instead, e.g. a single specific\n distro:\n\n rig proj lock --platform ubuntu-24.04\n\n Use \u{1b}[32m--add-platform\u{1b}[39m instead to add a platform to that default set rather\n than replacing it, e.g. to also solve for one extra distro on top of the\n usual five. \u{1b}[32m--add-platform\u{1b}[39m can be repeated:\n\n rig proj lock --add-platform ubuntu-24.04 --add-platform linux-fedora-42\n\n \u{1b}[32m--platform\u{1b}[39m/\u{1b}[32m--add-platform\u{1b}[39m accept:\n\n - \u{1b}[32mmacos-arm64\u{1b}[39m, \u{1b}[32mwindows-x86_64\u{1b}[39m -- an OS plus arch, for the two\n non-Linux platforms.\n - \u{1b}[32mubuntu-24.04\u{1b}[39m, \u{1b}[32mfedora-42\u{1b}[39m, \u{1b}[32mopensuse-15.6\u{1b}[39m -- a Linux distro and version,\n matched against P3M's build list.\n - \u{1b}[32mmanylinux_2_28-arm64\u{1b}[39m, \u{1b}[32mjammy-x86_64\u{1b}[39m -- a P3M platform name and arch\n directly, e.g. copied from another \u{1b}[32mrproj.lock\u{1b}[39m's \u{1b}[32mplatform\u{1b}[39m field.\n - \u{1b}[32msource\u{1b}[39m -- source packages only, for any platform.\n\n A Linux distro/version or platform name P3M has no specific build for falls\n back to its generic manylinux build for the given arch, rather than failing.\n\n rig also falls back to source packages when there are no binaries for a\n platform at all.\n\n rig keeps the repository metadata and the binary package indices it solves\n from in its cache, and refreshes them once a day. Use \u{1b}[32m--no-cache\u{1b}[39m to ignore\n the cache, or clean the cache with \u{1b}[32mrig cached clean\u{1b}[39m.\n\n\u{1b}[1m\u{1b}[34mSticky lock files:\u{1b}[39m\u{1b}[22m\n A \u{1b}[32mrig proj lock\u{1b}[39m run that finds an existing \u{1b}[32mrproj.lock\u{1b}[39m already satisfying\n \u{1b}[32mrproj.toml\u{1b}[39m reuses it as-is, for every package, instead of re-resolving\n anything. This applies to ordinary dependencies, an existing pin that still\n satisfies the manifest's version requirement is kept, even if a newer\n version has since been published, as well as to git/GitHub dependencies (see\n below). Use \u{1b}[32mrig proj lock --upgrade\u{1b}[39m to ignore the existing lock file and\n re-resolve every dependency instead, picking the latest version that still\n satisfies \u{1b}[32mrproj.toml\u{1b}[39m.\n\n\u{1b}[1m\u{1b}[34mExcluding newer package versions:\u{1b}[39m\u{1b}[22m\n \u{1b}[32m--exclude-newer\u{1b}[39m makes the solver ignore CRAN package versions published\n after a date, so you can lock the project as CRAN was on that day, or skip\n versions released in the last few days:\n\n rig proj lock --exclude-newer 2025-06-01\n rig proj lock --exclude-newer \"7 days\"\n\n It takes a date (\u{1b}[32m2025-06-01\u{1b}[39m), an RFC 3339 timestamp (only its UTC date is\n used), or a span back from today, e.g. \u{1b}[32m7 days\u{1b}[39m, \u{1b}[32m2 weeks\u{1b}[39m or \u{1b}[32mP1M\u{1b}[39m. A\n version's publication date is the day it first appeared in a Posit Package\n Manager snapshot, so the cutoff is a day, and it cannot be earlier than\n 2017-10-10, the first snapshot. It only applies to CRAN packages, not to\n git, GitHub, URL or local dependencies, and binary builds of a version are\n used no matter when they were built.\n\n To make it part of the project, set it in \u{1b}[32mrproj.toml\u{1b}[39m; \u{1b}[32m--exclude-newer\u{1b}[39m\n overrides it:\n\n [tool.rig]\n exclude-newer = \"2025-06-01\"\n\n \u{1b}[32mrproj.lock\u{1b}[39m records the cutoff in its own \u{1b}[32m[tool.rig]\u{1b}[39m table, and a lock\n solved with a different one is not reused. For a span, the lock records the\n span as well, and the lock is reused as long as the span stays the same, so\n the cutoff does not move every day. Use \u{1b}[32m--upgrade\u{1b}[39m to solve again with a\n fresh cutoff.\n\n\u{1b}[1m\u{1b}[34mGit, GitHub and URL dependencies:\u{1b}[39m\u{1b}[22m\n A \u{1b}[32mgit::\u{1b}[39m/\u{1b}[32mgithub::\u{1b}[39m dependency pinned to a branch, a pull request, or no ref\n at all (the default branch's tip) is only resolved against its remote the\n first time it's locked. Once \u{1b}[32mrproj.lock\u{1b}[39m records a commit for it, later\n \u{1b}[32mrig proj lock\u{1b}[39m runs reuse that commit as-is rather than re-checking whether\n the branch moved on every lock. \u{1b}[32mrig proj lock --upgrade\u{1b}[39m re-checks every\n git/GitHub dependency's ref and moves the pin forward if it changed. A\n \u{1b}[32mrev\u{1b}[39m/\u{1b}[32mtag\u{1b}[39m pins an exact commit already, so there's nothing for \u{1b}[32m--upgrade\u{1b}[39m\n to move. \u{1b}[32mrelease = true\u{1b}[39m is sticky the same way: once locked, later runs\n keep the release it pinned instead of asking GitHub which release is latest\n every time. \u{1b}[32mrig proj lock --upgrade\u{1b}[39m re-checks and moves the pin forward if\n a newer release exists.\n\n A \u{1b}[32murl::\u{1b}[39m dependency names one exact archive rather than a movable ref, so\n there's nothing for \u{1b}[32m--upgrade\u{1b}[39m to move either: every \u{1b}[32mrig proj lock\u{1b}[39m run\n downloads it (the download itself is cached) and records its sha256 in\n \u{1b}[32mrproj.lock\u{1b}[39m, which then changes only if the archive's contents change. Set\n \u{1b}[32mhash\u{1b}[39m on a \u{1b}[32murl\u{1b}[39m dependency to pin the expected sha256 and catch that.\n\n The \u{1b}[32mrproj.lock\u{1b}[39m file records, for every package, whether it is a source or a\n binary package and the URL it is downloaded from. It also records where the\n file is cached, which is per \u{1b}[3mbuild\u{1b}[23m rather than per version: a repository\n can offer several binaries of one version for one platform and R version,\n and they are cached side by side.\n\n\u{1b}[1m\u{1b}[34mWorkspaces:\u{1b}[39m\u{1b}[22m\n A manifest with a \u{1b}[32m[workspace]\u{1b}[39m table is the root of a workspace: a monorepo\n of several projects or packages, listed as path patterns in \u{1b}[32mmembers\u{1b}[39m, that\n share one \u{1b}[32mrproj.lock\u{1b}[39m and one package library. \u{1b}[32mexclude\u{1b}[39m drops directories a\n \u{1b}[32mmembers\u{1b}[39m pattern would otherwise match, and the root manifest is always a\n member of its own workspace.\n\n \u{1b}[32mrig proj lock\u{1b}[39m in a workspace (from the root or from any member directory)\n reads every member and resolves them all in one solve, so that every member\n ends up with the same version of every shared dependency, and writes one\n \u{1b}[32mrproj.lock\u{1b}[39m at the workspace root. A member that depends on a sibling member\n is resolved against that sibling's own dependencies. The members themselves\n are directories rather than packages to download, so they are not recorded\n in the lock file.\n\n The R version rig solves for has to satisfy every member's \u{1b}[32mR\u{1b}[39m requirement,\n not just the root's.\n\n \u{1b}[32m[workspace.dependencies]\u{1b}[39m declares shared version requirements. A member\n inherits one by name, instead of spelling out its own requirement:\n\n # rproj.toml, the workspace root\n [workspace]\n members = [\"packages/*\"]\n\n [workspace.dependencies]\n cli = \">= 3.6.0\"\n\n # packages/mypkg/rproj.toml, a member\n [dependencies]\n cli = { workspace = true }\n\n An entry no member inherits has no effect on the solve; it is a\n declaration, not a request. Whether a member attaches a package (\u{1b}[32mattach\u{1b}[39m) is\n still the member's own business, and is kept when the rest of the entry is\n inherited."; +const HELP_PROJ_LOCK: &str = "\u{1b}[1m\u{1b}[34mDescription:\u{1b}[39m\u{1b}[22m\n Resolve the dependencies of an R project to a concrete set of package\n versions, and write the result to \u{1b}[32mrproj.lock\u{1b}[39m.\n\n rig reads the project manifest, \u{1b}[32mrproj.toml\u{1b}[39m, in the current directory, and\n uses its built-in solver to find a compatible set of package versions from\n the configured repositories.\n\n \u{1b}[32mrig proj solve\u{1b}[39m does not run R.\n\n Use \u{1b}[32m--r-version\u{1b}[39m to solve for a specific R version.\n\n\u{1b}[1m\u{1b}[34mOptional dependencies and dependency groups:\u{1b}[39m\u{1b}[22m\n Every \u{1b}[32m[dependency-groups.*]\u{1b}[39m table (\u{1b}[32mdev\u{1b}[39m, \u{1b}[32menhances\u{1b}[39m, or any other name)\n and every \u{1b}[32m[optional-dependencies.*]\u{1b}[39m extra are optional dependencies:\n packages the project suggests or can take advantage of, but does not need to\n run. \u{1b}[32mrig proj lock\u{1b}[39m always solves all of them together with the project's\n hard dependencies, in one solve, so \u{1b}[32mrproj.lock\u{1b}[39m is complete -- a version\n picked for a shared package is the same whether it got pulled in as a hard\n or an optional dependency, and every group and extra is available to install\n without a new solve. A \u{1b}[32m[dependency-groups.*]\u{1b}[39m table can also \u{1b}[32minclude-groups\n = [...]\u{1b}[39m other groups, pulling in their packages too; \u{1b}[32mrig proj lock\u{1b}[39m follows\n this when solving, and rejects a cycle (a group that includes itself,\n directly or through others).\n\n \u{1b}[32mrig proj sync\u{1b}[39m is where a subset of this is picked for\n installation -- by default \u{1b}[32mmain\u{1b}[39m plus the \u{1b}[32mdev\u{1b}[39m group, more with\n \u{1b}[32m--group\u{1b}[39m/\u{1b}[32m--all-groups\u{1b}[39m/\u{1b}[32m--extra\u{1b}[39m/\u{1b}[32m--all-extras\u{1b}[39m.\n\n\u{1b}[1m\u{1b}[34mThe R version:\u{1b}[39m\u{1b}[22m\n Without \u{1b}[32m--r-version\u{1b}[39m rig solves for the default R version, provided the\n manifest's own \u{1b}[32mR\u{1b}[39m requirement allows it. If it does not, rig takes the\n newest installed R version that does, and failing that the current R\n release. The version it picks does not have to be installed: \u{1b}[32mrig proj lock\u{1b}[39m\n never runs R, and \u{1b}[32mrig proj sync\u{1b}[39m installs the R version the lock file names.\n\n\u{1b}[1m\u{1b}[34mSource and binary packages:\u{1b}[39m\u{1b}[22m\n The solver considers binary packages as well as source packages, and\n prefers a binary build when one is available for the same version. By\n default a binary build never changes \u{1b}[3mwhich version\u{1b}[23m rig picks: the newest\n suitable version wins, and a binary of it is used if there is one. Use\n \u{1b}[32m--prefer-binary\u{1b}[39m to let an older version win instead, when the newest one\n has no binary but an older one does, typically because a version was\n released so recently that it has not been built yet. Only the three newest\n versions of a package are considered; \u{1b}[32m--prefer-binary=5\u{1b}[39m considers five.\n Versions held back this way are marked in the output.\n\n By default rig solves for this machine plus the three other common\n platforms (macOS arm64, Windows x86_64 and GNU Linux x86_64), and for\n source packages only, so the lock file also works on platforms without\n binary packages. \u{1b}[32mrig proj sync\u{1b}[39m only uses the source target if no other\n target matches the machine. Use\n \u{1b}[32m--platform\u{1b}[39m to solve for a different set instead, e.g. a single specific\n distro:\n\n rig proj lock --platform ubuntu-24.04\n\n Use \u{1b}[32m--add-platform\u{1b}[39m instead to add a platform to that default set rather\n than replacing it, e.g. to also solve for one extra distro on top of the\n usual five. \u{1b}[32m--add-platform\u{1b}[39m can be repeated:\n\n rig proj lock --add-platform ubuntu-24.04 --add-platform linux-fedora-42\n\n \u{1b}[32m--platform\u{1b}[39m/\u{1b}[32m--add-platform\u{1b}[39m accept:\n\n - \u{1b}[32mmacos-arm64\u{1b}[39m, \u{1b}[32mwindows-x86_64\u{1b}[39m -- an OS plus arch, for the two\n non-Linux platforms.\n - \u{1b}[32mubuntu-24.04\u{1b}[39m, \u{1b}[32mfedora-42\u{1b}[39m, \u{1b}[32mopensuse-15.6\u{1b}[39m -- a Linux distro and version,\n matched against P3M's build list.\n - \u{1b}[32mmanylinux_2_28-arm64\u{1b}[39m, \u{1b}[32mjammy-x86_64\u{1b}[39m -- a P3M platform name and arch\n directly, e.g. copied from another \u{1b}[32mrproj.lock\u{1b}[39m's \u{1b}[32mplatform\u{1b}[39m field.\n - \u{1b}[32msource\u{1b}[39m -- source packages only, for any platform.\n\n A Linux distro/version or platform name P3M has no specific build for falls\n back to its generic manylinux build for the given arch, rather than failing.\n\n rig also falls back to source packages when there are no binaries for a\n platform at all.\n\n rig keeps the repository metadata and the binary package indices it solves\n from in its cache, and refreshes them once a day. Use \u{1b}[32m--no-cache\u{1b}[39m to ignore\n the cache, or clean the cache with \u{1b}[32mrig cached clean\u{1b}[39m.\n\n\u{1b}[1m\u{1b}[34mSticky lock files:\u{1b}[39m\u{1b}[22m\n A \u{1b}[32mrig proj lock\u{1b}[39m run that finds an existing \u{1b}[32mrproj.lock\u{1b}[39m already satisfying\n \u{1b}[32mrproj.toml\u{1b}[39m reuses it as-is, for every package, instead of re-resolving\n anything. This applies to ordinary dependencies, an existing pin that still\n satisfies the manifest's version requirement is kept, even if a newer\n version has since been published, as well as to git/GitHub dependencies (see\n below).\n\n If \u{1b}[32mrproj.toml\u{1b}[39m changed, e.g. after \u{1b}[32mrig proj add\u{1b}[39m, rig solves the\n dependencies again, but it still keeps the versions \u{1b}[32mrproj.lock\u{1b}[39m pins, as\n long as they fit. So adding a package only changes the versions it needs\n changed, and does not upgrade the rest of the project. A platform or R\n version that is new to the lock file gets the same versions as the other\n platforms of the same R version, where possible.\n\n Use \u{1b}[32mrig proj lock --upgrade\u{1b}[39m to ignore the existing lock file and\n re-resolve every dependency instead, picking the latest version that still\n satisfies \u{1b}[32mrproj.toml\u{1b}[39m.\n\n Use \u{1b}[32m--upgrade-package\u{1b}[39m to upgrade only some packages, and keep the rest:\n\n rig proj lock --upgrade-package cli\n rig proj lock -P cli,glue\n\n The packages they depend on are only upgraded if they need to be. You can\n also give a version requirement, with the same \u{1b}[32m@\u{1b}[39m syntax\n as \u{1b}[32mrig proj add\u{1b}[39m, e.g. to upgrade or downgrade to a specific version:\n\n rig proj lock -P 'cli@>= 3.6.4'\n rig proj lock -P 'cli@=3.6.2'\n\n The requirement only applies to this run, it is not written to\n \u{1b}[32mrproj.toml\u{1b}[39m. Later \u{1b}[32mrig proj lock\u{1b}[39m runs keep the version it picked, as\n long as it fits \u{1b}[32mrproj.toml\u{1b}[39m. A git/GitHub dependency named in\n \u{1b}[32m--upgrade-package\u{1b}[39m is checked against its remote again, like with\n \u{1b}[32m--upgrade\u{1b}[39m.\n\n\u{1b}[1m\u{1b}[34mExcluding newer package versions:\u{1b}[39m\u{1b}[22m\n \u{1b}[32m--exclude-newer\u{1b}[39m makes the solver ignore CRAN package versions published\n after a date, so you can lock the project as CRAN was on that day, or skip\n versions released in the last few days:\n\n rig proj lock --exclude-newer 2025-06-01\n rig proj lock --exclude-newer \"7 days\"\n\n It takes a date (\u{1b}[32m2025-06-01\u{1b}[39m), an RFC 3339 timestamp (only its UTC date is\n used), or a span back from today, e.g. \u{1b}[32m7 days\u{1b}[39m, \u{1b}[32m2 weeks\u{1b}[39m or \u{1b}[32mP1M\u{1b}[39m. A\n version's publication date is the day it first appeared in a Posit Package\n Manager snapshot, so the cutoff is a day, and it cannot be earlier than\n 2017-10-10, the first snapshot. It only applies to CRAN packages, not to\n git, GitHub, URL or local dependencies, and binary builds of a version are\n used no matter when they were built.\n\n To make it part of the project, set it in \u{1b}[32mrproj.toml\u{1b}[39m; \u{1b}[32m--exclude-newer\u{1b}[39m\n overrides it:\n\n [tool.rig]\n exclude-newer = \"2025-06-01\"\n\n \u{1b}[32mrproj.lock\u{1b}[39m records the cutoff in its own \u{1b}[32m[tool.rig]\u{1b}[39m table, and a lock\n solved with a different one is solved again. This keeps the pinned versions\n that are not newer than the new cutoff, see \"Sticky lock files\" above. For a span, the lock records the\n span as well, and the lock is reused as long as the span stays the same, so\n the cutoff does not move every day. Use \u{1b}[32m--upgrade\u{1b}[39m to solve again with a\n fresh cutoff.\n\n\u{1b}[1m\u{1b}[34mGit, GitHub and URL dependencies:\u{1b}[39m\u{1b}[22m\n A \u{1b}[32mgit::\u{1b}[39m/\u{1b}[32mgithub::\u{1b}[39m dependency pinned to a branch, a pull request, or no ref\n at all (the default branch's tip) is only resolved against its remote the\n first time it's locked. Once \u{1b}[32mrproj.lock\u{1b}[39m records a commit for it, later\n \u{1b}[32mrig proj lock\u{1b}[39m runs reuse that commit as-is rather than re-checking whether\n the branch moved on every lock. \u{1b}[32mrig proj lock --upgrade\u{1b}[39m re-checks every\n git/GitHub dependency's ref and moves the pin forward if it changed. A\n \u{1b}[32mrev\u{1b}[39m/\u{1b}[32mtag\u{1b}[39m pins an exact commit already, so there's nothing for \u{1b}[32m--upgrade\u{1b}[39m\n to move. \u{1b}[32mrelease = true\u{1b}[39m is sticky the same way: once locked, later runs\n keep the release it pinned instead of asking GitHub which release is latest\n every time. \u{1b}[32mrig proj lock --upgrade\u{1b}[39m re-checks and moves the pin forward if\n a newer release exists.\n\n A \u{1b}[32murl::\u{1b}[39m dependency names one exact archive rather than a movable ref, so\n there's nothing for \u{1b}[32m--upgrade\u{1b}[39m to move either: every \u{1b}[32mrig proj lock\u{1b}[39m run\n downloads it (the download itself is cached) and records its sha256 in\n \u{1b}[32mrproj.lock\u{1b}[39m, which then changes only if the archive's contents change. Set\n \u{1b}[32mhash\u{1b}[39m on a \u{1b}[32murl\u{1b}[39m dependency to pin the expected sha256 and catch that.\n\n The \u{1b}[32mrproj.lock\u{1b}[39m file records, for every package, whether it is a source or a\n binary package and the URL it is downloaded from. It also records where the\n file is cached, which is per \u{1b}[3mbuild\u{1b}[23m rather than per version: a repository\n can offer several binaries of one version for one platform and R version,\n and they are cached side by side.\n\n\u{1b}[1m\u{1b}[34mWorkspaces:\u{1b}[39m\u{1b}[22m\n A manifest with a \u{1b}[32m[workspace]\u{1b}[39m table is the root of a workspace: a monorepo\n of several projects or packages, listed as path patterns in \u{1b}[32mmembers\u{1b}[39m, that\n share one \u{1b}[32mrproj.lock\u{1b}[39m and one package library. \u{1b}[32mexclude\u{1b}[39m drops directories a\n \u{1b}[32mmembers\u{1b}[39m pattern would otherwise match, and the root manifest is always a\n member of its own workspace.\n\n \u{1b}[32mrig proj lock\u{1b}[39m in a workspace (from the root or from any member directory)\n reads every member and resolves them all in one solve, so that every member\n ends up with the same version of every shared dependency, and writes one\n \u{1b}[32mrproj.lock\u{1b}[39m at the workspace root. A member that depends on a sibling member\n is resolved against that sibling's own dependencies. The members themselves\n are directories rather than packages to download, so they are not recorded\n in the lock file.\n\n The R version rig solves for has to satisfy every member's \u{1b}[32mR\u{1b}[39m requirement,\n not just the root's.\n\n \u{1b}[32m[workspace.dependencies]\u{1b}[39m declares shared version requirements. A member\n inherits one by name, instead of spelling out its own requirement:\n\n # rproj.toml, the workspace root\n [workspace]\n members = [\"packages/*\"]\n\n [workspace.dependencies]\n cli = \">= 3.6.0\"\n\n # packages/mypkg/rproj.toml, a member\n [dependencies]\n cli = { workspace = true }\n\n An entry no member inherits has no effect on the solve; it is a\n declaration, not a request. Whether a member attaches a package (\u{1b}[32mattach\u{1b}[39m) is\n still the member's own business, and is kept when the rest of the entry is\n inherited."; const ABOUT_PROJ_REMOVE: &str = "Remove dependencies from \u{1b}[32mrproj.toml\u{1b}[39m"; const HELP_PROJ_REMOVE: &str = "\u{1b}[1m\u{1b}[34mDescription:\u{1b}[39m\u{1b}[22m\n Remove one or more R packages from \u{1b}[32mrproj.toml\u{1b}[39m, rig's project and package\n manifest, then update \u{1b}[32mrproj.lock\u{1b}[39m and the project library to match. A\n package is removed wherever it is listed: \u{1b}[32m[dependencies]\u{1b}[39m,\n \u{1b}[32m[linking-dependencies]\u{1b}[39m, or any \u{1b}[32m[dependency-groups.*]\u{1b}[39m table.\n\n Naming a package that is not a dependency in \u{1b}[32mrproj.toml\u{1b}[39m is an error, and\n none of the named packages are removed if any of them is not found, so a\n typo cannot silently remove the wrong set of packages.\n\n\u{1b}[1m\u{1b}[34mOptions:\u{1b}[39m\u{1b}[22m\n \u{1b}[32m--no-sync\u{1b}[39m updates \u{1b}[32mrproj.toml\u{1b}[39m and \u{1b}[32mrproj.lock\u{1b}[39m, but does not touch the project\n library.\n\n \u{1b}[32m--no-lock\u{1b}[39m only updates \u{1b}[32mrproj.toml\u{1b}[39m. Nothing is resolved or installed, so\n this also works offline.\n\n\u{1b}[1m\u{1b}[34mFiles:\u{1b}[39m\u{1b}[22m\n Only \u{1b}[32mrproj.toml\u{1b}[39m is edited. Changes are merged into the existing file, so\n comments and custom formatting are preserved where possible.\n\n If resolving the remaining dependencies fails, \u{1b}[32mrproj.toml\u{1b}[39m is restored to\n what it was, so a failed \u{1b}[32mrig proj remove\u{1b}[39m does not leave the project with a\n manifest that cannot be locked."; const ABOUT_PROJ_RENV_EXPORT: &str = "Write an renv.lock file from rproj.toml"; @@ -126,7 +126,7 @@ const HELP_RTOOLS_RM: &str = "\u{1b}[1m\u{1b}[34mDescription:\u{1b}[39m\u{1b}[22 const ABOUT_RTOOLS: &str = "Manage Rtools installations"; const HELP_RTOOLS: &str = "\u{1b}[1m\u{1b}[34mDescription:\u{1b}[39m\u{1b}[22m\n Manage Rtools installations (Windows only).\n\n Rtools is the collection of build tools (compilers, \u{1b}[32mmake\u{1b}[39m, etc.) needed to\n build R packages from source on Windows. Each R version needs a matching\n Rtools version.\n\n \u{1b}[32mrig rtools list\u{1b}[39m lists the installed Rtools versions. \u{1b}[32mrig rtools add\u{1b}[39m\n installs Rtools, by default every version needed by the currently installed\n R versions. \u{1b}[32mrig rtools rm\u{1b}[39m removes Rtools versions.\n\n On non-Windows platforms this command does nothing and is hidden."; const ABOUT_RUN: &str = "Run R, an R script or an R project"; -const HELP_RUN: &str = "\u{1b}[1m\u{1b}[34mDescription:\u{1b}[39m\u{1b}[22m\n Run R, an R script or an R project, using the selected R version.\n\n All of these examples allow an \u{1b}[32m--r-version\u{1b}[39m argument, to use a specific R\n version.\n\n rig run # start R\n rig run # run an R script\n rig run -f # run an R script\n rig run -e # evaluate an R expression\n rig run ::