Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e329d60
plan
vangberg Nov 18, 2025
2e78f7a
Add targets backend foundation with generate_targets_file()
vangberg Nov 18, 2025
17c8e38
Integrate targets backend into run() function
vangberg Nov 18, 2025
8ada9db
Fix linting issues in run.R
vangberg Nov 18, 2025
4cf73cf
Integrate targets backend into status() function
vangberg Nov 18, 2025
b18c373
Modify clean() to use targets backend
vangberg Nov 18, 2025
7041c02
Add comprehensive integration tests for targets backend
vangberg Nov 18, 2025
f6719fb
Add backward compatibility documentation
vangberg Nov 18, 2025
751742e
Implement targets backend with vector-based output targets
vangberg Nov 18, 2025
938e871
Simplify to one target per script
vangberg Nov 18, 2025
d5c0dd5
Keep callr for script isolation in simplified targets
vangberg Nov 18, 2025
1e5255c
Add format='file' to output targets
vangberg Nov 18, 2025
e6d3434
Use callr::r() without library import
vangberg Nov 18, 2025
ff8ce9b
Fix status() to check output targets and display inputs/outputs
vangberg Nov 18, 2025
59215d9
Show external files in status() inputs
vangberg Nov 18, 2025
e18ca46
Display staleness/freshness in status() with nested list format
vangberg Nov 18, 2025
32f7143
Remove legacy display_scripts_table function
vangberg Nov 18, 2025
49a6dbf
Remove unused graph and state modules
vangberg Nov 18, 2025
dd65b83
Fix all tests to reflect current targets implementation
vangberg Nov 18, 2025
d059c92
Update README to describe Bakepipe as a targets frontend
vangberg Nov 18, 2025
7bacdc7
Fix priority 1 issues before merge
vangberg Nov 18, 2025
0b4d23d
Fix R CMD check issues (down to 1 intentional warning)
vangberg Nov 18, 2025
37b1f17
Remove migration documentation files
vangberg Nov 19, 2025
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ sample-project/analysis_results.rds
sample-project/report.txt
sample-project/.bakepipe.state

# Targets backend files
_targets.R
_targets/

# R files
.Rproj.user
.Rhistory
Expand Down
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
Imports: callr, fs
Imports: callr, fs, targets
Suggests: testthat (>= 3.0.0)
Config/testthat/edition: 3
URL: https://vangberg.github.io/bakepipe/, https://github.com/vangberg/bakepipe
Expand Down
3 changes: 1 addition & 2 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ export(clean)
export(external_in)
export(file_in)
export(file_out)
export(generate_targets_file)
export(run)
export(status)
importFrom(callr,r)
importFrom(fs,path_rel)
importFrom(stats,setNames)
importFrom(utils,read.csv)
importFrom(utils,write.csv)
105 changes: 31 additions & 74 deletions R/clean.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
#' intermediate files. This provides a complete clean of generated artifacts.
#' The pipeline can be regenerated by running run() again.
#'
#' @param verbose Logical. If TRUE (default), prints progress messages to console.
#' @param verbose Logical. If TRUE (default), prints progress messages
#' to console.
#' @return Character vector of file paths that were actually removed
#' @export
#' @examples
Expand All @@ -13,94 +14,50 @@
#' dir.create(temp_dir)
#' sample_proj <- system.file("extdata", "sample-project", package = "bakepipe")
#' file.copy(sample_proj, temp_dir, recursive = TRUE)
#'
#'
#' # Change to the sample project directory
#' old_wd <- getwd()
#' setwd(file.path(temp_dir, "sample-project"))
#'
#'
#' # Run the pipeline first to create output files
#' run(verbose = FALSE)
#'
#' run()
#'
#' # Now clean up the generated files
#' removed_files <- clean()
#' print(removed_files)
#'
#'
#' # Restore working directory and clean up
#' setwd(old_wd)
#' unlink(temp_dir, recursive = TRUE)
clean <- function(verbose = TRUE) {
# Parse all scripts to get dependencies
dependencies <- parse()

# If no scripts found, return empty vector
if (length(dependencies$scripts) == 0) {
if (verbose) {
message("\n[CLEAN] \033[1;36mBakepipe Clean\033[0m")
message("\033[33m No output files found to clean\033[0m\n")
deps <- parse()

# Collect all output files
files <- unique(unlist(lapply(deps$scripts, function(s) s$outputs)))

# Add targets artifacts
files <- c(files, "_targets.R")

# Track removed files
removed_files <- character()
removed <- 0
for (f in files) {
if (file.exists(f)) {
file.remove(f)
removed_files <- c(removed_files, f)
removed <- removed + 1
}
return(character(0))
}

# Get all outputs from the top-level outputs list
all_outputs <- dependencies$outputs

# Only try to remove files that actually exist
existing_files <- all_outputs[file.exists(all_outputs)]

# Print header
if (verbose) {
message("\n[CLEAN] \033[1;36mBakepipe Clean\033[0m")

if (length(existing_files) == 0) {
message("\033[32m[OK] No output files to clean - all clean!\033[0m\n")
return(character(0))
}

message(paste0("\033[33m Found ", length(existing_files), " output file",
if(length(existing_files) > 1) "s" else "", " to remove\033[0m\n"))
} else if (length(existing_files) == 0) {
return(character(0))
# Remove _targets directory
if (dir.exists("_targets")) {
unlink("_targets", recursive = TRUE)
removed <- removed + 1
}

# Calculate max filename width for alignment
max_width <- max(nchar(existing_files))

# Remove the files with progress indicators
removed_files <- character(0)
failed_files <- character(0)

for (file_path in existing_files) {
if (file.remove(file_path)) {
removed_files <- c(removed_files, file_path)
if (verbose) {
message(sprintf("\033[31m[RM] %s\033[0m", file_path))
}
} else {
failed_files <- c(failed_files, file_path)
if (verbose) {
message(sprintf("\033[33m[!] %-*s \033[2m(failed to remove)\033[0m", max_width, file_path))
}
}
}

# Print summary
if (verbose) {
message("\n\033[1;36m[SUMMARY]\033[0m")

if (length(removed_files) > 0) {
message(sprintf("\033[32m Removed %d file%s\033[0m",
length(removed_files),
if(length(removed_files) > 1) "s" else ""))
}

if (length(failed_files) > 0) {
message(sprintf("\033[33m Failed to remove %d file%s\033[0m",
length(failed_files),
if(length(failed_files) > 1) "s" else ""))
}

message("")
if (verbose && removed > 0) {
message(sprintf("βœ“ removed %d artifact%s", removed, if (removed != 1) "s" else ""))
}

removed_files
}
invisible(removed_files)
}
189 changes: 189 additions & 0 deletions R/generate_targets_file.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#' Generate _targets.R file from bakepipe scripts
#'
#' Parses all R scripts in the project and generates a _targets.R file
#' that defines targets for the targets package. This allows bakepipe
#' to use targets as a backend for pipeline execution.
#'
#' @return Invisibly returns the path to the generated _targets.R file
#' @importFrom callr r
#' @export
generate_targets_file <- function() {
# Parse all scripts to get dependencies
parsed <- parse()

# Get project root
project_root <- root()
targets_file <- file.path(project_root, "_targets.R")

# Start building the targets file content
lines <- character(0)

# Add library calls
lines <- c(lines, "library(targets)")
lines <- c(lines, "")

# Add the list of targets
lines <- c(lines, "list(")

# Generate targets for each script and its dependencies
target_lines <- character(0)

# Track all files that need file targets
all_file_targets <- character(0)

# First pass: add all external input file targets
for (script_name in names(parsed$scripts)) {
script_info <- parsed$scripts[[script_name]]

for (ext_file in script_info$externals) {
ext_target_name <- path_to_target_name(ext_file, "")
if (!(ext_target_name %in% all_file_targets)) {
target_lines <- c(
target_lines,
sprintf(
' tar_target(%s, "%s", format = "file"),',
ext_target_name,
ext_file
)
)
all_file_targets <- c(all_file_targets, ext_target_name)
}
}
}

# Second pass: generate one target per script
for (script_name in names(parsed$scripts)) {
script_info <- parsed$scripts[[script_name]]

# Generate target name from script path
output_target_name <- path_to_target_name(script_name, "output")

# Collect dependencies
deps <- character(0)

# Track the script itself so changes are detected
script_target_name <- path_to_target_name(script_name, "script")
target_lines <- c(
target_lines,
sprintf(
' tar_target(%s, "%s", format = "file"),',
script_target_name,
script_name
)
)
deps <- c(deps, script_target_name)

# Add external input dependencies
for (ext_file in script_info$externals) {
ext_target_name <- path_to_target_name(ext_file, "")
deps <- c(deps, ext_target_name)
}

# Add input file dependencies (from other scripts)
for (input_file in script_info$inputs) {
# Search through all scripts to find which one produces this input
for (producer_script in names(parsed$scripts)) {
if (input_file %in% parsed$scripts[[producer_script]]$outputs) {
# Found the producer script - depend on its output target
input_target_name <- path_to_target_name(producer_script, "output")
deps <- c(deps, input_target_name)
break
}
}
}

# Build the script target
target_start <- c(
sprintf(" tar_target("),
sprintf(" %s,", output_target_name),
sprintf(" {")
)

# Add dependencies
dep_lines <- character(0)
for (dep in deps) {
dep_lines <- c(dep_lines, sprintf(" %s", dep))
}

# Add script execution via callr for isolation
exec_lines <- c(
sprintf(" callr::r("),
sprintf(" func = function(script_path) {"),
sprintf(" source(script_path, local = TRUE)"),
sprintf(" },"),
sprintf(' args = list(script_path = "%s")', script_name),
sprintf(" )")
)

# Add output vector if script has outputs
output_lines <- character(0)
if (length(script_info$outputs) > 0) {
output_files_r <- paste0('"', script_info$outputs, '"', collapse = ", ")
output_lines <- c(output_lines, sprintf(" c(%s)", output_files_r))
} else {
# No outputs - return empty character vector
output_lines <- c(output_lines, sprintf(" character(0)"))
}

target_end <- c(
sprintf(" },"),
sprintf(" format = \"file\""),
sprintf(" ),")
)

# Combine all parts
full_target <- c(target_start, dep_lines, exec_lines, output_lines, target_end)
target_lines <- c(target_lines, full_target)

all_file_targets <- c(all_file_targets, output_target_name)
}

# Remove trailing comma from last target
if (length(target_lines) > 0) {
last_line <- target_lines[length(target_lines)]
target_lines[length(target_lines)] <- sub(",$", "", last_line)
}

# Combine everything
lines <- c(lines, target_lines)
lines <- c(lines, ")")

# Write to file
writeLines(lines, targets_file)

invisible(targets_file)
}

#' Convert file path to valid R target name
#'
#' Converts a file path to a valid R identifier for use as a target name.
#' Special characters are replaced with underscores.
#'
#' @param path File path
#' @param prefix Prefix to add (e.g., "script", "run", or "")
#' @return Valid R identifier
#' @keywords internal
path_to_target_name <- function(path, prefix = "") {
# Convert to lowercase and replace special chars with underscores
name <- tolower(path)

# Replace directory separators, dots (except in extensions), hyphens
# and other special characters with underscores
name <- gsub("[/\\.-]", "_", name)

# Remove any remaining non-alphanumeric characters
name <- gsub("[^a-z0-9_]", "_", name)

# Remove consecutive underscores
name <- gsub("_+", "_", name)

# Remove leading/trailing underscores
name <- gsub("^_|_$", "", name)

# Add prefix if provided
if (nzchar(prefix)) {
name <- paste(prefix, name, sep = "_")
}

name
}
Loading