Skip to content
Open
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
9 changes: 9 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# renv bootstrap artifacts (regenerated by setup.R)
renv_baseline/.Rprofile
renv_baseline/renv/

# Generated library trees
rv_equivalent/rv/

# renv cache
renv_baseline/renv/library/
51 changes: 51 additions & 0 deletions e2e/compare.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Compare renv.lock files generated by renv::snapshot() vs rv renv lock
# Run from e2e/ directory

renv_lock <- jsonlite::fromJSON("renv_baseline/renv.lock", simplifyVector = FALSE)
rv_lock <- jsonlite::fromJSON("rv_equivalent/renv.lock", simplifyVector = FALSE)

cat("=== R Section Comparison ===\n")
cat(sprintf(" renv R version: %s\n", renv_lock$R$Version))
cat(sprintf(" rv R version: %s\n", rv_lock$R$Version))
cat(sprintf(" renv repos: %s\n", jsonlite::toJSON(renv_lock$R$Repositories, auto_unbox = TRUE)))
cat(sprintf(" rv repos: %s\n", jsonlite::toJSON(rv_lock$R$Repositories, auto_unbox = TRUE)))

cat("\n=== Package Coverage ===\n")
renv_pkgs <- names(renv_lock$Packages)
rv_pkgs <- names(rv_lock$Packages)
cat(sprintf(" Only in renv (%d): %s\n", length(setdiff(renv_pkgs, rv_pkgs)), paste(setdiff(renv_pkgs, rv_pkgs), collapse = ", ")))
cat(sprintf(" Only in rv (%d): %s\n", length(setdiff(rv_pkgs, renv_pkgs)), paste(setdiff(rv_pkgs, renv_pkgs), collapse = ", ")))
cat(sprintf(" In both (%d): %s\n", length(intersect(renv_pkgs, rv_pkgs)), paste(intersect(renv_pkgs, rv_pkgs), collapse = ", ")))

cat("\n=== Per-Package Field Comparison ===\n")
shared_pkgs <- intersect(renv_pkgs, rv_pkgs)
for (pkg in sort(shared_pkgs)) {
renv_pkg <- renv_lock$Packages[[pkg]]
rv_pkg <- rv_lock$Packages[[pkg]]
all_keys <- sort(unique(c(names(renv_pkg), names(rv_pkg))))

diffs <- list()
matches <- character()
for (key in all_keys) {
renv_val <- renv_pkg[[key]]
rv_val <- rv_pkg[[key]]
if (identical(renv_val, rv_val)) {
matches <- c(matches, key)
} else {
diffs[[key]] <- list(renv = renv_val, rv = rv_val)
}
}

if (length(diffs) == 0) {
cat(sprintf("\n--- %s: IDENTICAL (%d fields) ---\n", pkg, length(matches)))
} else {
cat(sprintf("\n--- %s: %d match, %d differ ---\n", pkg, length(matches), length(diffs)))
cat(sprintf(" Matching: %s\n", paste(matches, collapse = ", ")))
for (key in names(diffs)) {
d <- diffs[[key]]
renv_str <- if (is.null(d$renv)) "(absent)" else jsonlite::toJSON(d$renv, auto_unbox = TRUE)
rv_str <- if (is.null(d$rv)) "(absent)" else jsonlite::toJSON(d$rv, auto_unbox = TRUE)
cat(sprintf(" DIFF %s:\n renv: %s\n rv: %s\n", key, renv_str, rv_str))
}
}
}
28 changes: 28 additions & 0 deletions e2e/renv_baseline/captured_descriptions/R6.DESCRIPTION
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Package: R6
Title: Encapsulated Classes with Reference Semantics
Version: 2.6.1
Authors@R: c( person("Winston", "Chang", , "winston@posit.co", role = c("aut", "cre")), person("Posit Software, PBC", role = c("cph", "fnd")) )
Description: Creates classes with reference semantics, similar to R's built-in reference classes. Compared to reference classes, R6 classes are simpler and lighter-weight, and they are not built on S4 classes so they do not require the methods package. These classes allow public and private members, and they support inheritance, even when the classes are defined in different packages.
License: MIT + file LICENSE
URL: https://r6.r-lib.org, https://github.com/r-lib/R6
BugReports: https://github.com/r-lib/R6/issues
Depends: R (>= 3.6)
Suggests: lobstr, testthat (>= 3.0.0)
Config/Needs/website: tidyverse/tidytemplate, ggplot2, microbenchmark, scales
Config/testthat/edition: 3
Encoding: UTF-8
RoxygenNote: 7.3.2
NeedsCompilation: no
Packaged: 2025-02-14 21:15:19 UTC; winston
Author: Winston Chang [aut, cre], Posit Software, PBC [cph, fnd]
Maintainer: Winston Chang <winston@posit.co>
Repository: RSPM
Date/Publication: 2025-02-15 00:50:02 UTC
Built: R 4.5.0; ; 2025-04-05 02:55:50 UTC; unix
RemoteType: standard
RemoteRef: R6
RemotePkgRef: R6
RemoteRepos: https://packagemanager.posit.co/cran/__linux__/noble/2025-07-15
RemoteReposName: CRAN
RemotePkgPlatform: source
RemoteSha: 2.6.1
23 changes: 23 additions & 0 deletions e2e/renv_baseline/captured_descriptions/rv.git.pkgA.DESCRIPTION
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Package: rv.git.pkgA
Title: Package Which Has No Dependencies
Version: 0.0.5
Authors@R: person("Wes", "Cummings", , "wes@a2-ai.com", role = c("aut", "cre"))
Description: This package has no dependencies
License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
Author: Wes Cummings [aut, cre]
Maintainer: Wes Cummings <wes@a2-ai.com>
Built: R 4.5.2; ; 2026-03-21 09:47:22 UTC; unix
RemoteType: github
RemoteHost: api.github.com
RemoteUsername: A2-ai
RemoteRepo: rv.git.pkgA
RemoteRef: v0.0.6
RemoteSha: cbc24e97b857305558ad5a4769086922812627cc
GithubHost: api.github.com
GithubRepo: rv.git.pkgA
GithubUsername: A2-ai
GithubRef: v0.0.6
GithubSHA1: cbc24e97b857305558ad5a4769086922812627cc
575 changes: 575 additions & 0 deletions e2e/renv_baseline/renv.lock

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions e2e/renv_baseline/setup.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# E2E test: Create a reference renv.lock using native renv
# This installs R6 from CRAN and rv.git.pkgA from GitHub (tag v0.0.6)
# then runs renv::snapshot() to produce the "gold standard" renv.lock

# Use a fixed CRAN snapshot for reproducibility
options(repos = c(CRAN = "https://packagemanager.posit.co/cran/__linux__/noble/2025-07-15/"))

# Initialize renv in this directory
renv::init(bare = TRUE, restart = FALSE)

# Install R6 from CRAN
renv::install("R6")

# Install rv.git.pkgA from GitHub at tag v0.0.6
renv::install("A2-ai/rv.git.pkgA@v0.0.6")

# Snapshot with type="all" to capture all installed packages (not just used ones)
renv::snapshot(type = "all", prompt = FALSE)

cat("renv.lock created successfully\n")

# Also dump the DESCRIPTION files for comparison
desc_dir <- file.path("captured_descriptions")
dir.create(desc_dir, showWarnings = FALSE)

lib_path <- renv::paths$library()
for (pkg in c("R6", "rv.git.pkgA")) {
desc_file <- file.path(lib_path, pkg, "DESCRIPTION")
if (file.exists(desc_file)) {
file.copy(desc_file, file.path(desc_dir, paste0(pkg, ".DESCRIPTION")), overwrite = TRUE)
cat(sprintf("Captured DESCRIPTION for %s\n", pkg))
} else {
cat(sprintf("WARNING: DESCRIPTION not found for %s at %s\n", pkg, desc_file))
}
}
59 changes: 59 additions & 0 deletions e2e/rv_equivalent/renv.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"Packages": {
"R6": {
"Author": "Winston Chang [aut, cre], Posit Software, PBC [cph, fnd]",
"Authors@R": "c( person(\"Winston\", \"Chang\", , \"winston@posit.co\", role = c(\"aut\", \"cre\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")) )",
"BugReports": "https://github.com/r-lib/R6/issues",
"Config/Needs/website": "tidyverse/tidytemplate, ggplot2, microbenchmark, scales",
"Config/testthat/edition": "3",
"Depends": [
"R (>= 3.6)"
],
"Description": "Creates classes with reference semantics, similar to R's built-in reference classes. Compared to reference classes, R6 classes are simpler and lighter-weight, and they are not built on S4 classes so they do not require the methods package. These classes allow public and private members, and they support inheritance, even when the classes are defined in different packages.",
"Encoding": "UTF-8",
"License": "MIT + file LICENSE",
"Maintainer": "Winston Chang <winston@posit.co>",
"NeedsCompilation": "no",
"Package": "R6",
"Repository": "CRAN",
"RoxygenNote": "7.3.2",
"Source": "Repository",
"Suggests": [
"lobstr",
"testthat (>= 3.0.0)"
],
"Title": "Encapsulated Classes with Reference Semantics",
"URL": "https://r6.r-lib.org, https://github.com/r-lib/R6",
"Version": "2.6.1"
},
"rv.git.pkgA": {
"Author": "Wes Cummings [aut, cre]",
"Authors@R": "person(\"Wes\", \"Cummings\", , \"wes@a2-ai.com\", role = c(\"aut\", \"cre\"))",
"Description": "This package has no dependencies",
"Encoding": "UTF-8",
"License": "MIT + file LICENSE",
"Maintainer": "Wes Cummings <wes@a2-ai.com>",
"Package": "rv.git.pkgA",
"RemoteHost": "api.github.com",
"RemoteRef": "v0.0.6",
"RemoteRepo": "rv.git.pkgA",
"RemoteSha": "cbc24e97b857305558ad5a4769086922812627cc",
"RemoteType": "github",
"RemoteUsername": "A2-ai",
"Roxygen": "list(markdown = TRUE)",
"RoxygenNote": "7.3.2",
"Source": "GitHub",
"Title": "Package Which Has No Dependencies",
"Version": "0.0.5"
}
},
"R": {
"Repositories": [
{
"Name": "CRAN",
"URL": "https://packagemanager.posit.co/cran/__linux__/noble/2025-07-15"
}
],
"Version": "4.5"
}
}
10 changes: 10 additions & 0 deletions e2e/rv_equivalent/rproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
name = "e2e-renv-comparison"
r_version = "4.5"
repositories = [
{ alias = "CRAN", url = "https://packagemanager.posit.co/cran/__linux__/noble/2025-07-15/" }
]
dependencies = [
"R6",
{ name = "rv.git.pkgA", git = "https://github.com/A2-ai/rv.git.pkgA", tag = "v0.0.6" },
]
18 changes: 18 additions & 0 deletions e2e/rv_equivalent/rv.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# This file is automatically @generated by rv.
# It is not intended for manual editing.
version = 2
r_version = "4.5"

[[packages]]
name = "R6"
version = "2.6.1"
source = { repository = "https://packagemanager.posit.co/cran/__linux__/noble/2025-07-15" }
force_source = false
dependencies = []

[[packages]]
name = "rv.git.pkgA"
version = "0.0.5"
source = { git = "https://github.com/A2-ai/rv.git.pkgA", sha = "cbc24e97b857305558ad5a4769086922812627cc", tag = "v0.0.6" }
force_source = true
dependencies = []
2 changes: 2 additions & 0 deletions src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod init;
mod migrate;
mod renv;
mod tree;

pub use init::{find_r_repositories, init, init_structure};
pub use migrate::migrate_renv;
pub use renv::generate_renv_lock;
pub use tree::tree;
1 change: 1 addition & 0 deletions src/cli/commands/renv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub use crate::renv_lock::generate_renv_lock;
4 changes: 3 additions & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ mod sync;
pub mod utils;

pub use crate::{Context, RCommandLookup, ResolveMode};
pub use commands::{find_r_repositories, init, init_structure, migrate_renv, tree};
pub use commands::{
find_r_repositories, generate_renv_lock, init, init_structure, migrate_renv, tree,
};
pub use resolution::resolve_dependencies;
pub use sync::SyncHelper;
pub use utils::OutputFormat;
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod project_summary;
mod r_cmd;
pub mod r_finder;
mod renv;
pub mod renv_lock;
mod repository;
mod repository_urls;
mod resolver;
Expand Down
8 changes: 8 additions & 0 deletions src/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,10 @@ impl Lockfile {
})
}

pub fn packages(&self) -> &[LockedPackage] {
&self.packages
}

/// Gets a set of all the package names listed in the lockfile
pub fn package_names(&self) -> HashSet<&str> {
let mut out = HashSet::new();
Expand All @@ -613,6 +617,10 @@ impl Lockfile {
Version::from_str(&self.r_version.to_string()).unwrap()
}

pub fn r_version_str(&self) -> &str {
&self.r_version
}

pub fn version(&self) -> i64 {
self.version
}
Expand Down
68 changes: 66 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use serde_json::json;

use anyhow::anyhow;
use rv::cli::{
Context, OutputFormat, RCommandLookup, ResolveMode, SyncHelper, find_r_repositories, init,
init_structure, migrate_renv, resolve_dependencies, tree,
Context, OutputFormat, RCommandLookup, ResolveMode, SyncHelper, find_r_repositories,
generate_renv_lock, init, init_structure, migrate_renv, resolve_dependencies, tree,
};
use rv::r_finder::get_r_from_path;
use rv::system_req::{SysDep, SysInstallationStatus};
Expand Down Expand Up @@ -68,6 +68,11 @@ pub enum Command {
#[clap(subcommand)]
subcommand: MigrateSubcommand,
},
/// Generate renv-compatible files
Renv {
#[clap(subcommand)]
subcommand: RenvSubcommand,
},
/// Replaces the library with exactly what is in the lock file
Sync {
#[clap(long)]
Expand Down Expand Up @@ -304,6 +309,23 @@ pub enum MigrateSubcommand {
},
}

#[derive(Debug, Subcommand)]
pub enum RenvSubcommand {
/// Generate an renv.lock file from rv.lock and installed library
Lock {
/// Output file path
#[clap(long, default_value = "renv.lock")]
output: PathBuf,
/// Packages to exclude from renv.lock (comma-separated). Must be top-level dependencies in rproject.toml.
/// Transitive dependencies only needed by excluded packages are also removed.
#[clap(long, value_delimiter = ',')]
exclude_pkgs: Vec<String>,
/// Show what would be excluded without writing the file
#[clap(long)]
dry_run: bool,
},
}

fn try_main() -> Result<()> {
let cli = Cli::parse();
let output_format = if cli.json {
Expand Down Expand Up @@ -444,6 +466,48 @@ fn try_main() -> Result<()> {
}
}
}
Command::Renv {
subcommand:
RenvSubcommand::Lock {
output,
exclude_pkgs,
dry_run,
},
} => {
let context = Context::new(&cli.config_file, RCommandLookup::Strict)
.map_err(|e| anyhow!("{e}"))?;
let lockfile = rv::Lockfile::load(context.lockfile_path())
.map_err(|e| anyhow!("{e}"))?
.ok_or_else(|| anyhow!("Lockfile is outdated or missing. Run `rv sync` first."))?;

if dry_run {
let report = rv::renv_lock::compute_exclusion_report(
&lockfile,
&context.config,
&exclude_pkgs,
)?;
if output_format.is_json() {
println!("{}", serde_json::to_string_pretty(&report.to_json())?);
} else {
print!("{report}");
}
} else {
let renv_lock = generate_renv_lock(
&lockfile,
&context.config,
context.library_path(),
&exclude_pkgs,
)?;
let json_string = serde_json::to_string_pretty(&renv_lock)?;
fs_err::write(&output, format!("{json_string}\n"))?;

if output_format.is_json() {
println!("{}", json!({"output": output.display().to_string()}));
} else {
println!("renv.lock generated at {}", output.display());
}
}
}
Command::Sync {
save_install_logs_in,
} => {
Expand Down
Loading
Loading