Skip to content

Commit ea680c7

Browse files
committed
Add from-environment
1 parent 7dcb272 commit ea680c7

17 files changed

+968
-39
lines changed

.Rbuildignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
^\.github$
77
^dockitect\.Rproj$
88
^\.devcontainer$
9+
^\.Rproj\.user$

NAMESPACE

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ S3method(print,dockerfile)
55
S3method(print,dockerignore)
66
export(check_dockerfile)
77
export(check_dockerignore)
8+
export(determine_os)
9+
export(determine_package_manager)
810
export(dfi_add)
911
export(dfi_arg)
1012
export(dfi_cmd)
@@ -26,6 +28,11 @@ export(dfi_workdir)
2628
export(di_add)
2729
export(di_remove)
2830
export(di_replace)
31+
export(dk_add_sysreqs)
32+
export(dk_from_description)
33+
export(dk_from_renv)
34+
export(dk_from_script)
35+
export(dk_from_session)
2936
export(dk_template_ignore_common)
3037
export(dk_template_ignore_data)
3138
export(dk_template_ignore_editor)
@@ -39,7 +46,7 @@ export(dk_template_ignore_raw_data)
3946
export(dk_template_ignore_renv)
4047
export(dockerfile)
4148
export(dockerignore)
42-
export(get_package_manager)
49+
export(generate_pkg_install_cmd)
4350
export(has_instruction)
4451
export(is_dockerfile)
4552
export(is_dockerignore)

R/dockitect-from-environment.R

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#' Create a dockerfile from the current R session
2+
#'
3+
#' @param base_image Base image to use (default: rocker/r-ver with current R version)
4+
#' @param include_packages Include loaded packages (default: TRUE)
5+
#' @param include_sysreqs Include system requirements for packages (default: TRUE)
6+
#' @param package_manager Package manager to use (default: auto-detected)
7+
#' @return A dockerfile object
8+
#' @export
9+
dk_from_session <- function(base_image = NULL, include_packages = TRUE,
10+
include_sysreqs = TRUE, package_manager = "auto") {
11+
# Get current R version
12+
r_version <- paste(R.version$major, R.version$minor, sep = ".")
13+
14+
# Set default base image if not provided
15+
if (is.null(base_image)) {
16+
base_image <- paste0("rocker/r-ver:", r_version)
17+
}
18+
19+
# Create base dockerfile
20+
df <- dockerfile() |>
21+
dfi_from(base_image) |>
22+
dfi_label(maintainer = Sys.getenv("USER", "unknown"))
23+
24+
# Set package manager
25+
if (package_manager == "auto") {
26+
package_manager <- determine_package_manager(base_image)
27+
}
28+
29+
# Add loaded packages if requested
30+
if (include_packages) {
31+
# Get loaded packages from the current session
32+
pkgs <- sort(setdiff(
33+
# Get names of all loaded packages
34+
loadedNamespaces(),
35+
# Exclude base R packages and dockitect
36+
c("base", "compiler", "datasets", "graphics",
37+
"grDevices", "grid", "methods", "parallel",
38+
"splines", "stats", "stats4", "tcltk", "tools",
39+
"utils", "dockitect")
40+
))
41+
42+
# Add system requirements if requested
43+
if (include_sysreqs && length(pkgs) > 0) {
44+
df <- dk_add_sysreqs(df, pkgs, package_manager)
45+
}
46+
47+
# Add R package installation command
48+
if (length(pkgs) > 0) {
49+
pkg_list <- paste0(shQuote(pkgs), collapse = ", ")
50+
df <- dfi_run(df, paste0("R -e \"install.packages(c(", pkg_list, "), repos='https://cloud.r-project.org/')\""))
51+
}
52+
}
53+
54+
df
55+
}
56+
57+
#' Add system requirements for R packages to a dockerfile
58+
#'
59+
#' @param dockerfile A dockerfile object
60+
#' @param packages Character vector of package names
61+
#' @param package_manager Package manager to use (default: auto-detected)
62+
#' @return Updated dockerfile object
63+
#' @export
64+
dk_add_sysreqs <- function(dockerfile, packages, package_manager = "auto") {
65+
check_dockerfile(dockerfile)
66+
67+
if (package_manager == "auto") {
68+
package_manager <- dockerfile$metadata$package_manager
69+
if (is.null(package_manager)) {
70+
package_manager <- "apt"
71+
cli::cli_warn("Could not determine package manager. Defaulting to apt.")
72+
}
73+
}
74+
75+
# Get system requirements using pak
76+
if (!requireNamespace("pak", quietly = TRUE)) {
77+
cli::cli_warn("Package 'pak' is required to determine system requirements. Skipping.")
78+
return(dockerfile)
79+
}
80+
81+
# Map package manager to appropriate sysreqs platform
82+
os <- determine_os(dockerfile$metadata$base_image)
83+
platform <- map_to_sysreqs_platform(package_manager, os)
84+
85+
# Get system requirements data frame
86+
sysreqs_df <- pak::pkg_sysreqs(packages, sysreqs_platform = platform)
87+
88+
if (is.null(sysreqs_df$packages) || nrow(sysreqs_df$packages) == 0) {
89+
return(dockerfile)
90+
}
91+
92+
# Extract system packages from the data frame and remove duplicates
93+
system_packages <- unique(unlist(sysreqs_df$packages$system_packages))
94+
95+
if (length(system_packages) == 0) {
96+
return(dockerfile)
97+
}
98+
99+
# Generate system-specific install commands using utility function
100+
install_cmd <- generate_pkg_install_cmd(package_manager, system_packages)
101+
102+
# Add the installation command
103+
dockerfile <- dfi_run(dockerfile, paste(install_cmd, collapse = " && \\\n "))
104+
105+
dockerfile
106+
}
107+
108+
#' Create a dockerfile from an renv.lock file
109+
#'
110+
#' @param lock_file Path to renv.lock file
111+
#' @param r_version R version to use (default: from lock file)
112+
#' @param base_image Base image to use (default: determined from R version)
113+
#' @param include_sysreqs Include system requirements (default: TRUE)
114+
#' @return A dockerfile object
115+
#' @export
116+
dk_from_renv <- function(lock_file = "renv.lock", r_version = NULL,
117+
base_image = NULL, include_sysreqs = TRUE) {
118+
if (!file.exists(lock_file)) {
119+
cli::cli_abort("Lock file not found: {lock_file}")
120+
}
121+
122+
# Read lock file
123+
lock_data <- jsonlite::fromJSON(lock_file)
124+
125+
# Extract R version if not provided
126+
if (is.null(r_version) && !is.null(lock_data$R$Version)) {
127+
r_version <- lock_data$R$Version
128+
}
129+
130+
# Set default base image if not provided
131+
if (is.null(base_image) && !is.null(r_version)) {
132+
base_image <- paste0("rocker/r-ver:", r_version)
133+
} else if (is.null(base_image)) {
134+
base_image <- "rocker/r-ver:latest"
135+
cli::cli_warn("R version not specified. Using {base_image}")
136+
}
137+
138+
# Create base dockerfile
139+
df <- dockerfile() |>
140+
dfi_from(base_image) |>
141+
dfi_label(maintainer = Sys.getenv("USER", "unknown"))
142+
143+
# Add packages from lock file
144+
if (!is.null(lock_data$Packages) && length(lock_data$Packages) > 0) {
145+
pkgs <- names(lock_data$Packages)
146+
147+
# Add system requirements if requested
148+
if (include_sysreqs && length(pkgs) > 0) {
149+
df <- dk_add_sysreqs(df, pkgs)
150+
}
151+
152+
# Add renv initialization and restore
153+
df <- df |>
154+
dfi_workdir("/app") |>
155+
dfi_copy(lock_file, "/app/renv.lock") |>
156+
dfi_run(c(
157+
"R -e \"install.packages('renv', repos = 'https://cloud.r-project.org/')\"",
158+
"R -e \"renv::init()\"",
159+
"R -e \"renv::restore()\""
160+
))
161+
}
162+
163+
df
164+
}
165+
166+
#' Create a dockerfile from a DESCRIPTION file
167+
#'
168+
#' @param description_file Path to DESCRIPTION file
169+
#' @param r_version R version to use (default: from DESCRIPTION)
170+
#' @param base_image Base image to use (default: determined from R version)
171+
#' @param include_sysreqs Include system requirements (default: TRUE)
172+
#' @return A dockerfile object
173+
#' @export
174+
dk_from_description <- function(description_file = "DESCRIPTION", r_version = NULL,
175+
base_image = NULL, include_sysreqs = TRUE) {
176+
if (!file.exists(description_file)) {
177+
cli::cli_abort("DESCRIPTION file not found: {description_file}")
178+
}
179+
180+
# Read DESCRIPTION file
181+
desc_data <- read.dcf(description_file)
182+
183+
# Extract R version if not provided
184+
if (is.null(r_version) && "Depends" %in% colnames(desc_data)) {
185+
r_depends <- desc_data[1, "Depends"]
186+
r_ver_match <- regexpr("R \\(>= ([0-9\\.]+)\\)", r_depends)
187+
if (r_ver_match != -1) {
188+
r_version <- sub("R \\(>= ([0-9\\.]+)\\)", "\\1", regmatches(r_depends, r_ver_match))
189+
}
190+
}
191+
192+
# Set default base image if not provided
193+
if (is.null(base_image) && !is.null(r_version)) {
194+
base_image <- paste0("rocker/r-ver:", r_version)
195+
} else if (is.null(base_image)) {
196+
base_image <- "rocker/r-ver:latest"
197+
cli::cli_warn("R version not specified. Using {base_image}")
198+
}
199+
200+
# Create base dockerfile
201+
df <- dockerfile() |>
202+
dfi_from(base_image) |>
203+
dfi_label(maintainer = if ("Maintainer" %in% colnames(desc_data)) desc_data[1, "Maintainer"] else Sys.getenv("USER", "unknown"))
204+
205+
# Extract packages from Depends, Imports, and Suggests
206+
pkg_fields <- c("Depends", "Imports", "Suggests")
207+
pkgs <- character(0)
208+
209+
for (field in pkg_fields) {
210+
if (field %in% colnames(desc_data)) {
211+
field_content <- desc_data[1, field]
212+
if (!is.na(field_content)) {
213+
# Split on commas and extract package names
214+
field_pkgs <- strsplit(field_content, ",")[[1]]
215+
field_pkgs <- trimws(field_pkgs)
216+
# Remove version specifications and R dependency
217+
field_pkgs <- sub("\\s*\\(.*\\)$", "", field_pkgs)
218+
field_pkgs <- field_pkgs[!grepl("^R$", field_pkgs)]
219+
pkgs <- c(pkgs, field_pkgs)
220+
}
221+
}
222+
}
223+
224+
# Add system requirements if requested
225+
if (include_sysreqs && length(pkgs) > 0) {
226+
df <- dk_add_sysreqs(df, pkgs)
227+
}
228+
229+
# Add R package installation command
230+
if (length(pkgs) > 0) {
231+
pkg_list <- paste0(shQuote(pkgs), collapse = ", ")
232+
df <- dfi_run(df, paste0("R -e \"install.packages(c(", pkg_list, "), repos='https://cloud.r-project.org/')\""))
233+
}
234+
235+
# Add package directory
236+
pkg_name <- desc_data[1, "Package"]
237+
df <- df |>
238+
dfi_workdir("/app") |>
239+
dfi_copy(".", "/app/") |>
240+
dfi_run(paste0("R CMD INSTALL --no-multiarch --with-keep.source /app"))
241+
242+
df
243+
}
244+
245+
#' Create a dockerfile from an R script
246+
#'
247+
#' @param script_file Path to R script
248+
#' @param base_image Base image to use (default: latest rocker/r-ver)
249+
#' @param include_sysreqs Include system requirements (default: TRUE)
250+
#' @return A dockerfile object
251+
#' @export
252+
dk_from_script <- function(script_file, base_image = "rocker/r-ver:latest", include_sysreqs = TRUE) {
253+
if (!file.exists(script_file)) {
254+
cli::cli_abort("Script file not found: {script_file}")
255+
}
256+
257+
# Read script file
258+
script <- readLines(script_file)
259+
260+
# Extract library and require calls to identify packages
261+
# Consider adding a dependency on `renv` to handle this kind of detection.
262+
library_pattern <- "library\\(([^\\)]+)\\)"
263+
require_pattern <- "require\\(([^\\)]+)\\)"
264+
265+
library_matches <- regmatches(script, gregexpr(library_pattern, script))
266+
require_matches <- regmatches(script, gregexpr(require_pattern, script))
267+
268+
pkgs <- character(0)
269+
270+
# Process library calls
271+
for (match_list in library_matches) {
272+
if (length(match_list) > 0) {
273+
for (match in match_list) {
274+
pkg_name <- sub(library_pattern, "\\1", match)
275+
# Remove quotes if present
276+
pkg_name <- gsub("[\"']", "", pkg_name)
277+
pkgs <- c(pkgs, pkg_name)
278+
}
279+
}
280+
}
281+
282+
# Process require calls
283+
for (match_list in require_matches) {
284+
if (length(match_list) > 0) {
285+
for (match in match_list) {
286+
pkg_name <- sub(require_pattern, "\\1", match)
287+
# Remove quotes if present
288+
pkg_name <- gsub("[\"']", "", pkg_name)
289+
pkgs <- c(pkgs, pkg_name)
290+
}
291+
}
292+
}
293+
294+
# Create base dockerfile
295+
df <- dockerfile() |>
296+
dfi_from(base_image) |>
297+
dfi_label(maintainer = Sys.getenv("USER", "unknown"))
298+
299+
# Add system requirements if requested
300+
if (include_sysreqs && length(pkgs) > 0) {
301+
df <- dk_add_sysreqs(df, pkgs)
302+
}
303+
304+
# Add R package installation command
305+
if (length(pkgs) > 0) {
306+
pkgs <- unique(pkgs)
307+
pkg_list <- paste0(shQuote(pkgs), collapse = ", ")
308+
df <- dfi_run(df, paste0("R -e \"install.packages(c(", pkg_list, "), repos='https://cloud.r-project.org/')\""))
309+
}
310+
311+
# Add script execution
312+
script_name <- basename(script_file)
313+
df <- df |>
314+
dfi_workdir("/app") |>
315+
dfi_copy(script_file, paste0("/app/", script_name)) |>
316+
dfi_cmd(paste0("Rscript /app/", script_name))
317+
318+
df
319+
}

0 commit comments

Comments
 (0)