Add reproducible R package management system
- Add renv.lock with exact package versions for reproducibility
- Add package_manager.R for automated package installation/updates
- Add extract_current_versions.R for package discovery
- Configure renv environment for team collaboration
All team members can now run source('r_app/package_manager.R') to get identical package environment
This commit is contained in:
parent
6efcc8cfec
commit
4d6439d5e0
200
r_app/extract_current_versions.R
Normal file
200
r_app/extract_current_versions.R
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
#' Version Extractor for SmartCane Project
|
||||
#'
|
||||
#' This script scans your R scripts to find all used packages and extracts
|
||||
#' the currently installed versions. Use this to populate the package_manager.R
|
||||
#' with your actual working versions.
|
||||
#'
|
||||
#' Usage:
|
||||
#' source("extract_current_versions.R")
|
||||
#'
|
||||
#' Author: SmartCane Team
|
||||
#' Date: 2025-06-24
|
||||
|
||||
# =============================================================================
|
||||
# PACKAGE DISCOVERY
|
||||
# =============================================================================
|
||||
|
||||
#' Extract packages from R scripts
|
||||
extract_packages_from_scripts <- function(script_dir = ".") {
|
||||
# Find all R files
|
||||
r_files <- list.files(script_dir, pattern = "\\.(R|Rmd)$", recursive = TRUE, full.names = TRUE)
|
||||
|
||||
packages <- c()
|
||||
|
||||
for (file in r_files) {
|
||||
cat("Scanning:", file, "\n")
|
||||
|
||||
tryCatch({
|
||||
content <- readLines(file, warn = FALSE)
|
||||
|
||||
# Find library() calls
|
||||
library_matches <- regmatches(content, regexpr('library\\(["\']?([^"\'\\)]+)["\']?\\)', content))
|
||||
library_packages <- gsub('library\\(["\']?([^"\'\\)]+)["\']?\\)', '\\1', library_matches)
|
||||
library_packages <- library_packages[library_packages != ""]
|
||||
|
||||
# Find require() calls
|
||||
require_matches <- regmatches(content, regexpr('require\\(["\']?([^"\'\\)]+)["\']?\\)', content))
|
||||
require_packages <- gsub('require\\(["\']?([^"\'\\)]+)["\']?\\)', '\\1', require_matches)
|
||||
require_packages <- require_packages[require_packages != ""]
|
||||
|
||||
# Find package::function calls
|
||||
namespace_matches <- regmatches(content, gregexpr('[a-zA-Z][a-zA-Z0-9.]*::', content))
|
||||
namespace_packages <- unique(unlist(lapply(namespace_matches, function(x) gsub('::', '', x))))
|
||||
namespace_packages <- namespace_packages[namespace_packages != ""]
|
||||
|
||||
packages <- c(packages, library_packages, require_packages, namespace_packages)
|
||||
|
||||
}, error = function(e) {
|
||||
cat("Error reading", file, ":", e$message, "\n")
|
||||
})
|
||||
}
|
||||
|
||||
# Clean and deduplicate
|
||||
packages <- unique(packages)
|
||||
packages <- packages[!packages %in% c("", "base", "stats", "utils", "graphics", "grDevices")]
|
||||
|
||||
return(sort(packages))
|
||||
}
|
||||
|
||||
#' Get current version of installed packages
|
||||
get_current_versions <- function(packages) {
|
||||
versions <- list()
|
||||
|
||||
cat("\nChecking installed versions...\n")
|
||||
cat("===============================\n")
|
||||
|
||||
for (pkg in packages) {
|
||||
if (pkg %in% rownames(installed.packages())) {
|
||||
version <- as.character(packageVersion(pkg))
|
||||
versions[[pkg]] <- version
|
||||
cat(sprintf("✓ %-20s %s\n", pkg, version))
|
||||
} else {
|
||||
cat(sprintf("✗ %-20s NOT INSTALLED\n", pkg))
|
||||
}
|
||||
}
|
||||
|
||||
return(versions)
|
||||
}
|
||||
|
||||
#' Generate package manager configuration
|
||||
generate_package_config <- function(versions) {
|
||||
cat("\n\nGenerating REQUIRED_PACKAGES configuration...\n")
|
||||
cat("=============================================\n\n")
|
||||
|
||||
config_lines <- c(
|
||||
"# Package requirements with your current working versions",
|
||||
"REQUIRED_PACKAGES <- list("
|
||||
)
|
||||
|
||||
# Group packages by category
|
||||
categories <- list(
|
||||
"Core data manipulation" = c("here", "dplyr", "tidyr", "readr", "readxl", "magrittr", "lubridate", "stringr"),
|
||||
"Spatial data" = c("sf", "terra", "exactextractr", "raster", "sp", "sf", "rgdal", "rgeos"),
|
||||
"Visualization" = c("tmap", "ggplot2", "RColorBrewer", "viridis", "scales"),
|
||||
"Statistical analysis" = c("lme4", "nlme", "mgcv", "survival", "cluster"),
|
||||
"Reporting" = c("knitr", "rmarkdown", "officedown", "officer", "flextable"),
|
||||
"Tidyverse" = c("tidyverse", "purrr", "forcats", "tibble"),
|
||||
"Other packages" = c()
|
||||
)
|
||||
|
||||
# Categorize packages
|
||||
categorized <- list()
|
||||
uncategorized <- names(versions)
|
||||
|
||||
for (category in names(categories)) {
|
||||
cat_packages <- intersect(names(versions), categories[[category]])
|
||||
if (length(cat_packages) > 0) {
|
||||
categorized[[category]] <- cat_packages
|
||||
uncategorized <- setdiff(uncategorized, cat_packages)
|
||||
}
|
||||
}
|
||||
|
||||
# Add uncategorized packages
|
||||
if (length(uncategorized) > 0) {
|
||||
categorized[["Other packages"]] <- uncategorized
|
||||
}
|
||||
|
||||
# Generate config
|
||||
for (category in names(categorized)) {
|
||||
config_lines <- c(config_lines, paste0(" # ", category))
|
||||
|
||||
packages_in_cat <- categorized[[category]]
|
||||
for (i in seq_along(packages_in_cat)) {
|
||||
pkg <- packages_in_cat[i]
|
||||
version <- versions[[pkg]]
|
||||
comma <- if (i == length(packages_in_cat) && category == names(categorized)[length(categorized)]) "" else ","
|
||||
|
||||
# Add special comment for critical packages
|
||||
comment <- ""
|
||||
if (pkg == "tmap") comment <- " # CRITICAL: for tm_scale_continuous() syntax"
|
||||
if (pkg == "terra") comment <- " # CRITICAL: for raster processing"
|
||||
|
||||
config_lines <- c(config_lines, sprintf(' "%s" = "%s"%s%s', pkg, version, comma, comment))
|
||||
}
|
||||
|
||||
if (category != names(categorized)[length(categorized)]) {
|
||||
config_lines <- c(config_lines, "")
|
||||
}
|
||||
}
|
||||
|
||||
config_lines <- c(config_lines, ")")
|
||||
|
||||
# Print to console
|
||||
cat(paste(config_lines, collapse = "\n"))
|
||||
|
||||
# Save to file
|
||||
writeLines(config_lines, "generated_package_config.R")
|
||||
cat("\n\n📁 Configuration saved to: generated_package_config.R\n")
|
||||
|
||||
return(config_lines)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN EXECUTION
|
||||
# =============================================================================
|
||||
|
||||
main <- function() {
|
||||
cat("🔍 SmartCane Package Version Extractor\n")
|
||||
cat("======================================\n\n")
|
||||
|
||||
# Step 1: Find all packages used in scripts
|
||||
cat("Step 1: Scanning R scripts for package usage...\n")
|
||||
packages <- extract_packages_from_scripts()
|
||||
|
||||
cat("\nFound packages:\n")
|
||||
cat(paste(packages, collapse = ", "), "\n")
|
||||
cat("\nTotal packages found:", length(packages), "\n")
|
||||
|
||||
# Step 2: Get current versions
|
||||
cat("\nStep 2: Checking installed versions...\n")
|
||||
versions <- get_current_versions(packages)
|
||||
|
||||
installed_count <- length(versions)
|
||||
missing_count <- length(packages) - installed_count
|
||||
|
||||
cat(sprintf("\n📊 Summary: %d installed, %d missing\n", installed_count, missing_count))
|
||||
|
||||
if (missing_count > 0) {
|
||||
missing_packages <- setdiff(packages, names(versions))
|
||||
cat("\n⚠️ Missing packages:\n")
|
||||
cat(paste(missing_packages, collapse = ", "), "\n")
|
||||
cat("\nYou may want to install these first, then re-run this script.\n")
|
||||
}
|
||||
|
||||
# Step 3: Generate configuration
|
||||
if (length(versions) > 0) {
|
||||
cat("\nStep 3: Generating package manager configuration...\n")
|
||||
config <- generate_package_config(versions)
|
||||
|
||||
cat("\n✅ Next steps:\n")
|
||||
cat("1. Review generated_package_config.R\n")
|
||||
cat("2. Copy the REQUIRED_PACKAGES list to package_manager.R\n")
|
||||
cat("3. Adjust any versions as needed\n")
|
||||
cat("4. Run package_manager.R\n")
|
||||
} else {
|
||||
cat("\n❌ No installed packages found. Install packages first.\n")
|
||||
}
|
||||
}
|
||||
|
||||
# Run the extraction
|
||||
main()
|
||||
315
r_app/package_manager.R
Normal file
315
r_app/package_manager.R
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
#' Package Manager for SmartCane Project
|
||||
#'
|
||||
#' This script manages R package versions across development, testing, and production environments.
|
||||
#' It uses renv for reproducible environments and ensures consistent package versions.
|
||||
#'
|
||||
#' Usage:
|
||||
#' source("package_manager.R")
|
||||
#'
|
||||
#' Author: SmartCane Team
|
||||
#' Date: 2025-06-24
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Package requirements with your current working versions
|
||||
REQUIRED_PACKAGES <- list(
|
||||
# Core data manipulation
|
||||
"dplyr" = "1.1.4",
|
||||
"here" = "1.0.1",
|
||||
"lubridate" = "1.9.4",
|
||||
"readr" = "2.1.5",
|
||||
"readxl" = "1.4.5",
|
||||
"stringr" = "1.5.1",
|
||||
"tidyr" = "1.3.1",
|
||||
"purrr" = "1.0.2",
|
||||
"magrittr" = "2.0.0", # Adding this as it's commonly used
|
||||
|
||||
# Spatial data
|
||||
"exactextractr" = "0.10.0",
|
||||
"raster" = "3.6.32",
|
||||
"sf" = "1.0.19",
|
||||
"terra" = "1.8.43", # CRITICAL: for raster processing
|
||||
|
||||
# Visualization - CRITICAL: tmap v4 for new syntax
|
||||
"ggplot2" = "3.5.1",
|
||||
"tmap" = "4.0", # CRITICAL: for tm_scale_continuous() syntax
|
||||
"gridExtra" = "2.3",
|
||||
# Reporting
|
||||
"knitr" = "1.50",
|
||||
"rmarkdown" = "2.21.0", # Adding this as it's needed for reports
|
||||
|
||||
# Tidyverse meta-package
|
||||
"tidyverse" = "2.0.0",
|
||||
|
||||
# Machine Learning & Statistics
|
||||
"caret" = "7.0.1",
|
||||
"CAST" = "1.0.3",
|
||||
"randomForest" = "4.7.1.2",
|
||||
"rsample" = "1.3.0",
|
||||
|
||||
# Parallel processing
|
||||
"furrr" = "0.3.1",
|
||||
"future" = "1.40.0",
|
||||
"progressr" = "0.15.1",
|
||||
|
||||
# Other utilities
|
||||
"reshape2" = "1.4.4",
|
||||
"zoo" = "1.8.13"
|
||||
)
|
||||
|
||||
# Log file setup
|
||||
LOG_FILE <- file.path(getwd(), "package_manager.log")
|
||||
START_TIME <- Sys.time()
|
||||
|
||||
# =============================================================================
|
||||
# UTILITY FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
#' Log message to both console and file
|
||||
log_message <- function(message, level = "INFO") {
|
||||
timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
|
||||
formatted_msg <- sprintf("[%s] %s - %s", level, timestamp, message)
|
||||
|
||||
# Print to console
|
||||
cat(formatted_msg, "\n")
|
||||
|
||||
# Write to log file
|
||||
cat(formatted_msg, "\n", file = LOG_FILE, append = TRUE)
|
||||
}
|
||||
|
||||
#' Check if package is installed
|
||||
is_package_installed <- function(package) {
|
||||
package %in% rownames(installed.packages())
|
||||
}
|
||||
|
||||
#' Get installed package version
|
||||
get_package_version <- function(package) {
|
||||
if (is_package_installed(package)) {
|
||||
as.character(packageVersion(package))
|
||||
} else {
|
||||
NULL
|
||||
}
|
||||
}
|
||||
|
||||
#' Compare version strings (returns TRUE if installed >= required)
|
||||
version_meets_requirement <- function(installed, required) {
|
||||
if (is.null(installed)) return(FALSE)
|
||||
utils::compareVersion(installed, required) >= 0
|
||||
}
|
||||
|
||||
#' Install or update package to minimum version
|
||||
install_or_update_package <- function(package, required_version) {
|
||||
current_version <- get_package_version(package)
|
||||
|
||||
if (is.null(current_version)) {
|
||||
log_message(sprintf("Installing %s (required: >= %s)", package, required_version))
|
||||
tryCatch({
|
||||
install.packages(package, dependencies = TRUE, quiet = TRUE)
|
||||
new_version <- get_package_version(package)
|
||||
log_message(sprintf("✓ Installed %s version %s", package, new_version), "SUCCESS")
|
||||
return(TRUE)
|
||||
}, error = function(e) {
|
||||
log_message(sprintf("✗ Failed to install %s: %s", package, e$message), "ERROR")
|
||||
return(FALSE)
|
||||
})
|
||||
} else if (!version_meets_requirement(current_version, required_version)) {
|
||||
log_message(sprintf("Updating %s from %s to >= %s", package, current_version, required_version))
|
||||
tryCatch({
|
||||
install.packages(package, dependencies = TRUE, quiet = TRUE)
|
||||
new_version <- get_package_version(package)
|
||||
if (version_meets_requirement(new_version, required_version)) {
|
||||
log_message(sprintf("✓ Updated %s to version %s", package, new_version), "SUCCESS")
|
||||
return(TRUE)
|
||||
} else {
|
||||
log_message(sprintf("⚠ %s updated to %s but still below required %s", package, new_version, required_version), "WARNING")
|
||||
return(FALSE)
|
||||
}
|
||||
}, error = function(e) {
|
||||
log_message(sprintf("✗ Failed to update %s: %s", package, e$message), "ERROR")
|
||||
return(FALSE)
|
||||
})
|
||||
} else {
|
||||
log_message(sprintf("✓ %s version %s meets requirement (>= %s)", package, current_version, required_version))
|
||||
return(TRUE)
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN PACKAGE MANAGEMENT FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
#' Initialize renv if not already initialized
|
||||
initialize_renv <- function() {
|
||||
log_message("Checking renv initialization...")
|
||||
|
||||
if (!file.exists("renv.lock")) {
|
||||
log_message("Initializing renv for the first time...")
|
||||
if (!requireNamespace("renv", quietly = TRUE)) {
|
||||
log_message("Installing renv...")
|
||||
install.packages("renv")
|
||||
}
|
||||
renv::init()
|
||||
log_message("✓ renv initialized", "SUCCESS")
|
||||
} else {
|
||||
log_message("✓ renv already initialized")
|
||||
# Check if renv is already active by looking at the library path
|
||||
if (!requireNamespace("renv", quietly = TRUE)) {
|
||||
install.packages("renv")
|
||||
}
|
||||
# Check if we're already using the renv project library
|
||||
lib_paths <- .libPaths()
|
||||
|
||||
if (!any(grepl("renv", lib_paths))) {
|
||||
log_message("Activating renv...")
|
||||
renv::activate()
|
||||
log_message("✓ renv activated")
|
||||
} else {
|
||||
log_message("✓ renv already active")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#' Check and install all required packages
|
||||
manage_packages <- function() {
|
||||
log_message("=== PACKAGE MANAGEMENT STARTED ===")
|
||||
log_message(sprintf("R version: %s", R.version.string))
|
||||
|
||||
success_count <- 0
|
||||
failure_count <- 0
|
||||
|
||||
for (package in names(REQUIRED_PACKAGES)) {
|
||||
required_version <- REQUIRED_PACKAGES[[package]]
|
||||
|
||||
if (install_or_update_package(package, required_version)) {
|
||||
success_count <- success_count + 1
|
||||
} else {
|
||||
failure_count <- failure_count + 1
|
||||
}
|
||||
}
|
||||
|
||||
log_message(sprintf("Package management complete: %d success, %d failures", success_count, failure_count))
|
||||
|
||||
if (failure_count > 0) {
|
||||
log_message("Some packages failed to install/update. Check log for details.", "WARNING")
|
||||
}
|
||||
|
||||
return(failure_count == 0)
|
||||
}
|
||||
|
||||
#' Update renv lockfile with current package versions
|
||||
update_lockfile <- function() {
|
||||
log_message("Updating renv lockfile...")
|
||||
tryCatch({
|
||||
renv::snapshot(prompt = FALSE)
|
||||
log_message("✓ renv lockfile updated", "SUCCESS")
|
||||
}, error = function(e) {
|
||||
log_message(sprintf("✗ Failed to update lockfile: %s", e$message), "ERROR")
|
||||
})
|
||||
}
|
||||
|
||||
#' Generate package report
|
||||
generate_package_report <- function() {
|
||||
log_message("=== PACKAGE REPORT ===")
|
||||
|
||||
# Check each required package
|
||||
for (package in names(REQUIRED_PACKAGES)) {
|
||||
required_version <- REQUIRED_PACKAGES[[package]]
|
||||
current_version <- get_package_version(package)
|
||||
|
||||
if (is.null(current_version)) {
|
||||
status <- "❌ NOT INSTALLED"
|
||||
} else if (version_meets_requirement(current_version, required_version)) {
|
||||
status <- "✅ OK"
|
||||
} else {
|
||||
status <- "⚠️ VERSION TOO OLD"
|
||||
}
|
||||
|
||||
log_message(sprintf("%-20s | Required: >= %-8s | Installed: %-8s | %s",
|
||||
package, required_version,
|
||||
ifelse(is.null(current_version), "NONE", current_version),
|
||||
status))
|
||||
}
|
||||
|
||||
log_message("=== END PACKAGE REPORT ===")
|
||||
}
|
||||
|
||||
#' Main function to run complete package management
|
||||
run_package_manager <- function() {
|
||||
# Initialize log
|
||||
cat("", file = LOG_FILE) # Clear log file
|
||||
log_message("SmartCane Project - Package Manager Started")
|
||||
log_message(sprintf("Working directory: %s", getwd()))
|
||||
|
||||
# Step 1: Initialize renv
|
||||
initialize_renv()
|
||||
|
||||
# Step 2: Generate initial report
|
||||
log_message("\n=== INITIAL STATE ===")
|
||||
generate_package_report()
|
||||
|
||||
# Step 3: Manage packages
|
||||
log_message("\n=== PACKAGE INSTALLATION/UPDATES ===")
|
||||
success <- manage_packages()
|
||||
|
||||
# Step 4: Update lockfile if successful
|
||||
if (success) {
|
||||
update_lockfile()
|
||||
}
|
||||
|
||||
# Step 5: Generate final report
|
||||
log_message("\n=== FINAL STATE ===")
|
||||
generate_package_report()
|
||||
|
||||
# Summary
|
||||
end_time <- Sys.time()
|
||||
duration <- round(as.numeric(difftime(end_time, START_TIME, units = "secs")), 2)
|
||||
|
||||
log_message(sprintf("Package management completed in %s seconds", duration))
|
||||
log_message(sprintf("Log saved to: %s", LOG_FILE))
|
||||
|
||||
if (success) {
|
||||
log_message("🎉 All packages successfully managed!", "SUCCESS")
|
||||
log_message("📋 Next steps:")
|
||||
log_message(" 1. Test your R scripts to ensure everything works")
|
||||
log_message(" 2. Commit renv.lock to version control")
|
||||
log_message(" 3. Share this script with your team")
|
||||
} else {
|
||||
log_message("⚠️ Some issues occurred. Check the log for details.", "WARNING")
|
||||
log_message("💡 You may need to:")
|
||||
log_message(" 1. Update R to a newer version")
|
||||
log_message(" 2. Install system dependencies")
|
||||
log_message(" 3. Check your internet connection")
|
||||
}
|
||||
|
||||
return(success)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# EXECUTION
|
||||
# =============================================================================
|
||||
|
||||
# Only run if script is sourced directly (not when loaded as module)
|
||||
if (!exists("PACKAGE_MANAGER_LOADED")) {
|
||||
PACKAGE_MANAGER_LOADED <- TRUE
|
||||
|
||||
cat("🚀 SmartCane Package Manager\n")
|
||||
cat("============================\n")
|
||||
cat("This will check and install/update all required R packages.\n")
|
||||
cat("Log file:", LOG_FILE, "\n\n")
|
||||
|
||||
# Ask for confirmation
|
||||
response <- readline("Continue? (y/N): ")
|
||||
if (tolower(substr(response, 1, 1)) == "y") {
|
||||
result <- run_package_manager()
|
||||
|
||||
if (result) {
|
||||
cat("\n✅ Package management completed successfully!\n")
|
||||
} else {
|
||||
cat("\n❌ Package management completed with errors. Check the log.\n")
|
||||
}
|
||||
} else {
|
||||
cat("❌ Package management cancelled.\n")
|
||||
}
|
||||
}
|
||||
1334
renv/activate.R
Normal file
1334
renv/activate.R
Normal file
File diff suppressed because it is too large
Load diff
19
renv/settings.json
Normal file
19
renv/settings.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"bioconductor.version": null,
|
||||
"external.libraries": [],
|
||||
"ignored.packages": [],
|
||||
"package.dependency.fields": [
|
||||
"Imports",
|
||||
"Depends",
|
||||
"LinkingTo"
|
||||
],
|
||||
"ppm.enabled": null,
|
||||
"ppm.ignored.urls": [],
|
||||
"r.version": null,
|
||||
"snapshot.type": "implicit",
|
||||
"use.cache": true,
|
||||
"vcs.ignore.cellar": true,
|
||||
"vcs.ignore.library": true,
|
||||
"vcs.ignore.local": true,
|
||||
"vcs.manage.ignores": true
|
||||
}
|
||||
Loading…
Reference in a new issue