From: Benjamin Auder Date: Wed, 21 May 2014 15:58:04 +0000 (+0200) Subject: initial commit X-Git-Url: https://git.auder.net/doc/html/%7B%7B%20targetUrl%20%7D%7D?a=commitdiff_plain;h=156f8ca651789536f3c0efd2af6c97f8b8e9eb54;p=pkgdev.git initial commit --- 156f8ca651789536f3c0efd2af6c97f8b8e9eb54 diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100755 index 0000000..df83ff4 --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,12 @@ +Package: pkgdev +Type: Package +Version: 0.0.1 +Date: 2013-XX-XX +Title: Package development helper +Author: Benjamin Auder +Maintainer: Benjamin Auder +Depends: R (>= 2.15.1) +Description: Allow nested subfolders in R/ directory, + and somewhat simplify testing process +License: GPL (>= 3) +LazyLoad: yes diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..f6b4a56 --- /dev/null +++ b/NAMESPACE @@ -0,0 +1,5 @@ +# Export all R functions +export (pkgdev.wipeAll,pkgdev.load,pkgdev.unload,pkgdev.clean,pkgdev.rtest,pkgdev.ctest,pkgdev.tocran,pkgdev.check) + +# Import all packages listed as Imports or Depends +#import (methods) diff --git a/R/CunitTestsMacros.R b/R/CunitTestsMacros.R new file mode 100644 index 0000000..77872c0 --- /dev/null +++ b/R/CunitTestsMacros.R @@ -0,0 +1,21 @@ +# Functions to assert that a condition is true or false in C tests + +# Macros to pass file name and line number (in user files [TOFIX]) +#define ASSERT_TRUE(x,y) assertTrue(__FILE__,__LINE__,x,y) +#define ASSERT_FALSE(x,y) assertFalse(__FILE__,__LINE__,x,y) + +CunitTestsMacros = ' +#include +#include + +// Functions to assert that a condition is true or false [to be extended] +void assertTrue(char* fileName, int lineNumber, int condition, char* message) { + if (!condition) { + printf(">>> Failure in file %s at line %i: %s\\n", fileName, lineNumber, message); + } +} + +void assertFalse(char* fileName, int lineNumber, int condition, char* message) { + assertTrue(fileName, lineNumber, !condition, message); +} +' diff --git a/R/RunitTestsMethods.R b/R/RunitTestsMethods.R new file mode 100644 index 0000000..67c31dd --- /dev/null +++ b/R/RunitTestsMethods.R @@ -0,0 +1,16 @@ +# Functions to assert that conditions are true or false in R unit tests + +RunitTestsMethods = ' +# Functions to assert that a condition is true or false [to be extended] +assertTrue = function(condition, message, context="") { + if (!condition) { + cat("Failure: ", message, sep=\'\') + if (context != "") cat(" [",context,"]") + cat("\\n") + } +} + +assertFalse = function(condition, message, context="") { + assertTrue(!condition, message, context) +} +' diff --git a/R/load.R b/R/load.R new file mode 100644 index 0000000..4f74839 --- /dev/null +++ b/R/load.R @@ -0,0 +1,130 @@ +# Core function to load a package (source R files and compile/load C libraries) +# @param path Location of the (non-standard) package to be loaded +# @param cc Compilator to be used (e.g. 'gcc -std=gnu99' [default]) +.pkgdev.load = function(path, cc) { + + # Get package name from path + pathTokens = strsplit(path, c(.Platform$file.sep))[[1]] + pkgName = pathTokens[length(pathTokens)] + + # Create base directory for pkgName under R_HOME_USER/pkgdev/pkgs/pkgName (if not existing) + pkdev_path = file.path(Sys.getenv("R_HOME_USER"), "pkgdev") + dir.create(file.path(pkdev_path,"pkgs",pkgName), showWarnings=FALSE) + + # R code first + # Warning: R/tests folder should not be sourced + forbiddenPath = file.path(path,"R","tests") + for (fileOrDir in list.files( file.path(path,"R"),full.names=TRUE )) { + if (fileOrDir != forbiddenPath) { + if (file.info(fileOrDir)$isdir) { + rFiles = list.files(fileOrDir, pattern="\\.[RrSsq]$", + full.names=TRUE, recursive=TRUE) + # NOTE: potential unexported functions are not hidden; + # the developer is assumed to handle this + lapply(rFiles, source) + } + else source(fileOrDir) + } + } + + # Also load datasets (if any) + rData = list.files(file.path(path,"data"),pattern="\\.R(d|D)ata$",full.names=TRUE) + lapply(rData, load) + + # This file tells if the package is currently loaded + pkgLoadFile = file.path(pkdev_path,"pkgs",pkgName,"loaded") + + if (file.exists(file.path(path,"src"))) { + # C code -- Warning: src/tests folder should not be listed + cFiles = c( + list.files( file.path(path,"src","adapters"), pattern="\\.[cChH]$", + full.names=TRUE, recursive=TRUE ), + list.files( file.path(path,"src","sources"), pattern="\\.[cChH]$", + full.names=TRUE, recursive=TRUE )) + + # Create folder R_HOME_USER/pkgdev/pkgs/pkgName/src (if not existing) + dir.create(file.path(pkdev_path,"pkgs",pkgName,"src"), showWarnings=FALSE) + + # Generate suitable Makefile (all object files go at R_HOME_USER/pkgdev/pkgs/pkgName/src) + .generateMakefileLoad(path, cFiles, pkgName, cc) + + # Compile in the right folder (R_HOME_USER/pkgdev/pkgs/pkgName/src) + save_wd = getwd() + setwd( file.path(pkdev_path,"pkgs",pkgName,"src") ) + library(parallel) + system( paste( Sys.getenv("MAKE"), "depend", sep=' ') ) + system( paste( Sys.getenv("MAKE"), "-j", detectCores(), "all", sep=' ') ) + setwd(save_wd) + + # Finally load library + sharedLib = + file.path(pkdev_path,"pkgs",pkgName,paste(pkgName,.Platform$dynlib.ext,sep='')) + if (file.exists(pkgLoadFile)) dyn.unload(sharedLib) + dyn.load(sharedLib) + } + + # Mark package as 'loaded' + writeLines("loaded",pkgLoadFile) +} + +# Generate appropriate Makefile under R_HOME_USER/pkgdev/pkgs/pkgName/src +.generateMakefileLoad = function(path, cFiles, pkgName, cc) { + + # Preparation: separate cFiles into codes and headers + codeFiles = grep(".*(c|C)$", cFiles, value=TRUE) + headerFiles = grep(".*(h|H)$", cFiles, value=TRUE) + + # objectFiles = all .o files in current folder, duplicating file structure under path/src/ + basePathFrom = file.path(path, "src") + pkdev_path = file.path(Sys.getenv("R_HOME_USER"), "pkgdev") + basePathTo = file.path(pkdev_path,"pkgs",pkgName,"src") + for (fileOrDir in list.files(basePathFrom, recursive=TRUE, include.dirs=TRUE)) { + if (file.info(file.path(basePathFrom,fileOrDir))$isdir) { + # Process folders only + dir.create(file.path(basePathTo,fileOrDir),showWarnings=FALSE,recursive=TRUE) + } + } + objectFiles = c() + for (codeFile in codeFiles) { + objectFiles = c( + objectFiles, + sub("(.*)\\.(c|C)$","\\1\\.o", sub(basePathFrom,basePathTo,codeFile,fixed=TRUE))) + } + + # Build Makefile + makefile = paste(' +CC = ', cc, ' +INCLUDES = -I/usr/include/R/ -I/usr/local/include -I/usr/share/R/include +LIBRARIES = -L/usr/lib -L/usr/lib/R/lib -lR -lm +CFLAGS = -DNDEBUG -fpic -march=native -mtune=generic -O2 -pipe \\ + -fstack-protector --param=ssp-buffer-size=4 -D_FORTIFY_SOURCE=2 +LDFLAGS = -shared -Wl,-O1,--sort-common,--as-needed,-z,relro +LIB = ', paste(file.path("..",pkgName), .Platform$dynlib.ext, sep=''), ' +SRCS = ', paste(codeFiles, sep='', collapse=' '), ' +HEDS = ', paste(headerFiles, sep='', collapse=' '), ' +OBJS = ', paste(objectFiles, sep='', collapse= ' '), ' +all: $(LIB) +$(LIB) : $(OBJS) + $(CC) $(CFLAGS) $(LDFLAGS) $^ -o $(LIB) $(LIBRARIES)', sep='') + compileObjects = "" +for (i in 1:length(codeFiles)) { + compileObjects = paste(compileObjects, ' +', objectFiles[i], ' : ', codeFiles[i], ' + $(CC) $(INCLUDES) $(CFLAGS) -c $< -o $@', sep='') +} + makefile = paste(makefile, compileObjects, ' +.PHONY: clean delib depend +clean: + rm -f $(OBJS) ./.depend +delib: + rm -f $(LIB) +depend: .depend +.depend: $(SRCS) $(HEDS) + rm -f ./.depend + $(CC) -MM $^ > ./.depend +include .depend +', sep='') + + # Write it to disk + writeLines(makefile, file.path(pkdev_path,"pkgs",pkgName,"src","Makefile")) +} diff --git a/R/runCtests.R b/R/runCtests.R new file mode 100644 index 0000000..9bdc43b --- /dev/null +++ b/R/runCtests.R @@ -0,0 +1,180 @@ +# Core function to execute C unit tests (or just show functions names) +.pkgdev.ctest = function(path, prefix, show, cc) { + + # Initial step: list every potential unit test under path/src/tests. + allFuncNames = .parseCunitTests(file.path(path,"src","tests")) + + # Filter functions names matching prefix + funcNames = grep( paste("^test_",prefix,sep=''), allFuncNames, value=TRUE ) + if (length(funcNames) == 0) return #shortcut: nothing to do... + + # If show==TRUE, display every potential test starting with prefix, and exit + if (show) { + #display in alphabetic order + return (paste(sort(funcNames), sep='\n')) + } + + # Get package name from path + pathTokens = strsplit(path, c(.Platform$file.sep))[[1]] + pkgName = pathTokens[length(pathTokens)] + + # Generate main.c to run exactly the tests asked by user + .generateMainCall(funcNames, pkgName) + + # Get all C source files (src/tests/*, src/sources/* ...) + cFilesUser = c( + list.files(file.path(path,"src","tests"),pattern="\\.[cChH]$", + full.names=TRUE,recursive=TRUE), + list.files(file.path(path,"src","sources"),pattern="\\.[cChH]$", + full.names=TRUE,recursive=TRUE)) + pkdev_path = file.path(Sys.getenv("R_HOME_USER"), "pkgdev") + cFilesPkdev = c( + "main.c", #above generated main.c + file.path(pkdev_path,"src","tests","unitTestsMacros.c")) #C unit tests macros + + # Now generate appropriate Makefile based on C sources and headers + pathTokens = strsplit(path, c(.Platform$file.sep))[[1]] + pkgName = pathTokens[length(pathTokens)] + .generateMakefileTest(path, cFilesUser, cFilesPkdev, pkgName, cc) + + # Run selected tests (after 'funcs' filter applied) + pkdev_path = file.path(Sys.getenv("R_HOME_USER"), "pkgdev") + save_wd = getwd() + setwd( file.path(pkdev_path,"pkgs",pkgName,"src","tests") ) + library(parallel) + system( paste( Sys.getenv("MAKE"), "depend", sep=' ') ) + system( paste( Sys.getenv("MAKE"), "-j", detectCores(), "all", sep=' ') ) + system("./runTests") + setwd(save_wd) +} + +# Recursively explore initial path to parse source files for unit tests. +.parseCunitTests = function(path) { + + # Unit test names to return + funcNames = c() + + # For each file in current folder + for (fileName in list.files(path, full.names=TRUE, recursive=TRUE)) { + + # If the file is not a source, skip + if ( length( grep("\\.[CcHh]$", fileName) ) == 0) next + + # Every test function has a name starting with "test_" + matches = grep( + "^[ \t]*void[ \t]*test_[a-zA-Z0-9_]*[ \t]*\\(.*", + scan(fileName, what="character", sep='\n'), + value = TRUE) + + # We matched more to be 100% sure we got test functions, but need to strip now + funcNames = c(funcNames, sub( + "^[ \t]*void[ \t]*(test_[a-zA-Z0-9_]*)[ \t]*\\(.*", + "\\1", + matches)) + } + + return (funcNames) +} + +# Generate main.c file under R_HOME_USER/pkgdev/pkgs/pkgName/src/tests +.generateMainCall = function(funcNames, pkgName) { + + # Build text file main.c + mainDotC = ' +#include +#include +#include // to print timings + +void main() { + clock_t start, end; +' + for (funcName in funcNames) { + mainDotC = paste(mainDotC, "printf(\">>> Running ",funcName,"\\n\");\n",sep='') + mainDotC = paste(mainDotC, "start = clock();\n", sep='') + mainDotC = paste(mainDotC, funcName, "();\n", sep='') + mainDotC = paste(mainDotC, "end = clock();\n", sep='') + mainDotC = paste(mainDotC, "printf(\">>> ... completed in %.3fs.\\n\",((double) (end - start)) / CLOCKS_PER_SEC);\n", sep='') + } + mainDotC = paste(mainDotC, "}\n", sep='') + + # Write it on disk + pkdev_path = file.path(Sys.getenv("R_HOME_USER"), "pkgdev") + dir.create(file.path(pkdev_path,"pkgs",pkgName,"src","tests"), recursive=TRUE, showWarnings=FALSE) + writeLines(mainDotC, file.path(pkdev_path,"pkgs",pkgName,"src","tests","main.c")) +} + +# Generate appropriate Makefile under R_HOME_USER/pkgdev/pkgs/pkgName/src/tests +.generateMakefileTest = function(path, cFilesUser, cFilesPkdev, pkgName, cc) { + + # Preparation: separate cFiles into codes and headers + codeFilesUser = grep(".*(c|C)$", cFilesUser, value=TRUE) + codeFilesPkdev = grep(".*(c|C)$", cFilesPkdev, value=TRUE) + headerFiles = grep(".*(h|H)$", c(cFilesUser,cFilesPkdev), value=TRUE) + + # objectFiles = all .o files in current folder, duplicating file structure under path/src/ + basePathFrom = file.path(path, "src") + pkdev_path = file.path(Sys.getenv("R_HOME_USER"), "pkgdev") + basePathTo = file.path(pkdev_path,"pkgs",pkgName,"src","tests") + for (fileOrDir in list.files(basePathFrom, recursive=TRUE, include.dirs=TRUE)) { + if (file.info(file.path(basePathFrom,fileOrDir))$isdir) { + # Process folders only + dir.create(file.path(basePathTo,fileOrDir),showWarnings=FALSE,recursive=TRUE) + } + } + objectFiles = c() + for (codeFileUser in codeFilesUser) { + objectFiles = c( + objectFiles, + sub("(.*)\\.(c|C)$","\\1\\.o", sub(basePathFrom,basePathTo,codeFileUser,fixed=TRUE))) + } + for (codeFilePkdev in codeFilesPkdev) { + objectFiles = c( + objectFiles, + sub("(.*)\\.(c|C)$","\\1\\.o", codeFilePkdev)) + } + + # Build Makefile + makefile = paste(' +CC = ', cc, ' +INCLUDES = +LIBRARIES = -lm +CFLAGS = -g +LDFLAGS = +EXEC = runTests +SRCS = ', paste( + paste(codeFilesUser,sep='',collapse=' '), + paste(codeFilesPkdev,sep='',collapse=' '), + sep=' '), ' +HEDS = ', paste(headerFiles, sep='', collapse=' '), ' +OBJS = ', paste(objectFiles, sep='', collapse= ' '), ' +all: $(EXEC) +$(EXEC) : $(OBJS) + $(CC) $(CFLAGS) $(LDFLAGS) $^ -o $(EXEC) $(LIBRARIES)', sep='') + compileObjects = "" +lengthCodeFilesUser = length(codeFilesUser) +for (i in 1:lengthCodeFilesUser) { + compileObjects = paste(compileObjects, ' +', objectFiles[i], ' : ', codeFilesUser[i], ' + $(CC) $(INCLUDES) $(CFLAGS) -c $< -o $@', sep='') +} +for (i in 1:length(codeFilesPkdev)) { + compileObjects = paste(compileObjects, ' +', objectFiles[i+lengthCodeFilesUser], ' : ', codeFilesPkdev[i], ' + $(CC) $(INCLUDES) $(CFLAGS) -c $< -o $@', sep='') +} + makefile = paste(makefile, compileObjects, ' +.PHONY: clean delex depend +clean: + rm -f $(OBJS) ./.depend +delex: + rm -f $(EXEC) +depend: .depend +.depend: $(SRCS) $(HEDS) + rm -f ./.depend + $(CC) -MM $^ > ./.depend +include .depend +', sep='') + + # Write it to disk + writeLines(makefile, file.path(pkdev_path,"pkgs",pkgName,"src","tests","Makefile")) +} diff --git a/R/runRtests.R b/R/runRtests.R new file mode 100644 index 0000000..b649ead --- /dev/null +++ b/R/runRtests.R @@ -0,0 +1,75 @@ +# Core function to execute R unit tests (or just show functions names) +.pkgdev.rtest = function(path, prefix, show, cc) { + + # Initial step: list every potential unit test under path/R/tests. + allFuncNames = .parseRunitTests(file.path(path,"R","tests")) + + # Filter functions names matching prefix + funcNames = grep( paste("^test\\.",prefix,sep=''), allFuncNames, value=TRUE ) + if (length(funcNames) == 0) return #shortcut: nothing to do... + + # If show==TRUE, display every potential test starting with prefix, and exit + if (show) { + #display in alphabetic order + return (paste(sort(funcNames), sep='\n')) + } + + # Source all R unit test files + rFiles = list.files(file.path(path,"R","tests"), + pattern="\\.[RrSsq]$", full.names=TRUE, recursive=TRUE) + lapply(rFiles, source) + + # Source R file containing unit tests methods + pkdev_path = file.path(Sys.getenv("R_HOME_USER"), "pkgdev") + source(file.path(pkdev_path,"R","tests","unitTestsMethods.R")) + + # Get package name from path + pathTokens = strsplit(path, c(.Platform$file.sep))[[1]] + pkgName = pathTokens[length(pathTokens)] + + # This file tells if the package is currently loaded + pkgLoadFile = file.path(pkdev_path,"pkgs",pkgName,"loaded") + + # Now manually load package (may not be installable if non-flat) + pkgAlreadyLoaded = file.exists(pkgLoadFile) + .pkgdev.load(path, cc) + + # Run selected tests (after 'funcs' filter applied) + for (funcName in funcNames) { + cat(">>> Running ",funcName,"\n",sep='') + func = match.fun(funcName) + execTime = as.numeric(system.time(func())[1]) + cat(">>> ... completed in ",execTime,"s.\n",sep='') + } + + # Unload shared library if it wasn't already loaded + if (!pkgAlreadyLoaded) .pkgdev.unload(path) +} + +# Recursively explore initial path to parse source files for unit tests. +.parseRunitTests = function(path) { + + # Unit test names to return + funcNames = c() + + # For each file in current folder + for (fileName in list.files(path, full.names=TRUE, recursive=TRUE)) { + + # If the file is not a source, skip + if ( length( grep("\\.[RrSsq]$", fileName) ) == 0) next + + # Every test function has a name starting with "test." + matches = grep( + "^[ \t]*test\\.[a-zA-Z0-9_]*[ \t]*(=|<-)[ \t]*function.*", + scan(fileName, what="character", sep='\n', quiet=TRUE), + value = TRUE) + + # We matched more to be 100% sure we got test functions, but need to strip now + funcNames = c(funcNames, sub( + "^[ \t]*(test\\.[a-zA-Z0-9_]*)[ \t]*(=|<-)[ \t]*function.*", + "\\1", + matches)) + } + + return (funcNames) +} diff --git a/R/setup.R b/R/setup.R new file mode 100644 index 0000000..e95f9ca --- /dev/null +++ b/R/setup.R @@ -0,0 +1,35 @@ +# Setup a file structure under R_HOME/pkgdev/ to run later tests +# @param atInstall Logical, TRUE if invoked at package installation +.pkgdev.setup = function(reset=FALSE) { + + # environment variable R_HOME_USER must be set: everything starts here + if (Sys.getenv("R_HOME_USER") == "") { + cat("*** WARNING: for pkgdev to work properly, you need to specify\n") + cat("*** an environment variable R_HOME_USER in a .Renviron file.\n") + cat("*** Standard choice is /home/userName/.R under UNIX systems,\n") + cat("*** or maybe C:/Users/userName/Documents/R under Windows") + stop("Please specify R_HOME_USER before using pkgdev") + } + + # create convenient folders and files, if not already existing + pkdev_path = file.path(Sys.getenv("R_HOME_USER"), "pkgdev") + + # clean up: wipe possibly existing pkgdev/ contents + if (reset) { + unlink(pkdev_path, recursive=TRUE) + unlink(pkdev_path, recursive=TRUE) #bug? + } + + # copy file structure only if directory is absent + if (file.exists(pkdev_path)) return (NULL) + + # create testing file structure under pkgdev/ + dir.create(pkdev_path) + dir.create( file.path(pkdev_path,"R") ) + dir.create( file.path(pkdev_path,"R","tests") ) + writeLines(RunitTestsMethods, file.path(pkdev_path,"R","tests","unitTestsMethods.R")) + dir.create( file.path(pkdev_path,"src") ) + dir.create( file.path(pkdev_path,"src","tests") ) + writeLines(CunitTestsMacros, file.path(pkdev_path,"src","tests","unitTestsMacros.c")) + dir.create( file.path(pkdev_path,"pkgs") ) +} diff --git a/R/tocran.R b/R/tocran.R new file mode 100644 index 0000000..d6e4440 --- /dev/null +++ b/R/tocran.R @@ -0,0 +1,122 @@ +# Convert a package with custom sub-folders to a valid CRAN package +# @param inPath Input path: location of the package to flatten +# @param outPath Output path: location of the package to create +.pkgdev.tocran = function(inPath, outPath) { + + # Gather all R source files (no tests) + fullPathRfiles = c() + forbiddenPath = file.path(inPath,"R","tests") + for (fileOrDir in list.files(file.path(inPath,"R"),full.names=TRUE)) { + if (fileOrDir != forbiddenPath) { + if (file.info(fileOrDir)$isdir) { + fullPathRfiles = c( + fullPathRfiles, + list.files(fileOrDir,pattern="\\.[RrSsq]$", + recursive=TRUE,full.names=TRUE)) + } + else fullPathRfiles = c(fullPathRfiles, fileOrDir) + } + } + # Truncate paths: only suffix in pkgname/R/suffix is useful + fullPathRfiles = sub( + paste(inPath,.Platform$file.sep,"R",.Platform$file.sep,sep=''), + "", + fullPathRfiles, + fixed=TRUE) + + # Transform rFiles until no folder separator can be found + rFiles = fullPathRfiles + while (length(grep(.Platform$file.sep,rFiles)) > 0) { + rFiles = lowerFileDepth(rFiles) + } + + # Create and fill every non-sensible folder on output path + unlink(outPath, recursive=TRUE) #in case of [TODO: warn user] + dir.create(outPath, showWarnings=FALSE) + forbiddenPath = "R" + for (fileOrDir in list.files(inPath)) { + if (fileOrDir != forbiddenPath) { + if (file.info(file.path(inPath,fileOrDir))$isdir) { + dir.create(file.path(outPath,fileOrDir), showWarnings=FALSE, recursive=TRUE) + file.copy(file.path(inPath,fileOrDir), file.path(outPath),recursive=TRUE) + } + else file.copy(file.path(inPath,fileOrDir), file.path(outPath)) + } + } + + # Prepare R folder (empty for the moment) + dir.create( file.path(outPath,"R") ) + + # Copy "flattened" files to R/ + for (i in 1:length(rFiles)) { + file.copy( file.path(inPath,"R",fullPathRfiles[i]), + file.path(outPath,"R",rFiles[i]) ) + } + + # Optional processing if /src is present + if (file.exists(file.path(inPath,"src"))) { + + # Gather all C code files (NOT including headers; no tests) + cCodeFiles = c() + forbiddenPath = file.path(inPath,"src","tests") + for (fileOrDir in list.files(file.path(inPath,"src"),full.names=TRUE)) { + if (fileOrDir != forbiddenPath) { + if (file.info(fileOrDir)$isdir) { + cCodeFiles = c( + cCodeFiles, + list.files(fileOrDir,pattern="\\.[Cc]$", + recursive=TRUE,full.names=TRUE)) + } + else cCodeFiles = c(cCodeFiles, fileOrDir) + } + } + # Truncate paths: only suffix in pkgname/R/suffix is useful + cCodeFiles = sub( + paste(inPath,.Platform$file.sep,"src",.Platform$file.sep,sep=''), + "", + cCodeFiles, + fixed=TRUE) + + # Add a 'Makevars' file under src/ to allow compilation by R CMD INSTALL + makevars = paste( + paste("SOURCES","=",paste(cCodeFiles,sep='',collapse=' '),"\n",sep=' '), + paste("OBJECTS","=","$(SOURCES:.c=.o)","\n",sep=' '), + sep='\n') + writeLines(makevars, file.path(outPath,"src","Makevars")) + } +} + +# NOTE: rule = sort according to first subfolder, then place 1 for the first ...etc; +# for example tr1/tr2/tr3/file.c --> tr2/tr3/file_1.c --> tr3/file_12.c ...etc +lowerFileDepth = function(files) { + + # Sort files according to their prefix paths + sortedFiles = sort(files, index.return=TRUE) + + # Truncate paths if required + folderKount = 0 + lastFolder = "" + for (i in 1:length(files)) { + if (length(grep(.Platform$file.sep, sortedFiles$x[i], fixed=TRUE)) > 0) { + prefix = strsplit(sortedFiles$x[i], c(.Platform$file.sep))[[1]][1] + if (prefix != lastFolder) { + folderKount = folderKount + 1 + lastFolder = prefix + } + sortedFiles$x[i] = paste( + #truncate base name + substr(sortedFiles$x[i],nchar(prefix)+2,nchar(sortedFiles$x[i])-2), + #add [sub-]folder identifier + folderKount, + #add suffix (.R, .r ...) de + substr(sortedFiles$x[i],nchar(sortedFiles$x[i])-1,nchar(sortedFiles$x[i])), + sep='') + } + } + + # Return transformed files list ranked as in input (unsorted) + for (i in 1:length(files)) { + files[ sortedFiles$ix[i] ] = sortedFiles$x[i] + } + return (files) +} diff --git a/R/unload.R b/R/unload.R new file mode 100644 index 0000000..89d8152 --- /dev/null +++ b/R/unload.R @@ -0,0 +1,19 @@ +# Core function to unload a package (UNsource R files and unload C libraries) +# @param path Location or name of the (non-standard) package to be unloaded +.pkgdev.unload = function(path) { + + # Get package name from path + pathTokens = strsplit(path, c(.Platform$file.sep))[[1]] + pkgName = pathTokens[length(pathTokens)] + + # This file tells if the package is currently loaded + pkdev_path = file.path(Sys.getenv("R_HOME_USER"), "pkgdev") + pkgLoadFile = file.path(pkdev_path,"pkgs",pkgName,"loaded") + + # Unload shared library (if any) + if (file.exists(pkgLoadFile)) { + sharedLib = file.path(pkdev_path,"pkgs",pkgName,paste(pkgName,.Platform$dynlib.ext,sep='')) + dyn.unload(sharedLib) + unlink(pkgLoadFile) + } +} diff --git a/R/wrappers.R b/R/wrappers.R new file mode 100644 index 0000000..2af3302 --- /dev/null +++ b/R/wrappers.R @@ -0,0 +1,79 @@ +# Main wrapped entry point: call package function 'func' +.execMethod = function(func, ...) { + + # check for R_HOME_USER variable, and for R_HOME_USER/pkgdev folder + .pkgdev.setup() + + # get appropriate core function and call it + func = match.fun(func) + func(...) +} + +#' Reset pkgdev folder under R_HOME_USER +#' WARNING: all loaded packages will have to be rebuilt +pkgdev.wipeAll = function() { + .pkgdev.setup(reset=TRUE) +} + +#' Load a package containing arbitrary file structures under R/ and src/{adapters,sources} +#' +#' @param path Location of the package to load +#' @param cc Compilator to be used (e.g. 'gcc -std=gnu99' [default]) +pkgdev.load = function(path, cc="gcc -std=gnu99") { + path = normalizePath(path) #allow to give only pkg name (in pkg/.. folder) + .execMethod(".pkgdev.load", path, cc) +} + +#' Unload a package containing arbitrary file structures under R/ and src/{adapters,sources} +#' +#' @param path Location or name of the package to unload +pkgdev.unload = function(path) { + path = normalizePath(path) #allow to give only pkg name (in pkg/.. folder) + .execMethod(".pkgdev.unload", path) +} + +#' Wipe a specific package under R_HOME_USER/pkgdev/pkgs/ +#' NOTE: when this package will be loaded again, it will be completely rebuilt +#' +#' @param pkgName Name of the package to be removed +pkgdev.clean = function(pkgName) { + unlink(file.path(Sys.getenv("R_HOME_USER"),"pkgdev","pkgs",pkgName), recursive=TRUE) + unlink(file.path(Sys.getenv("R_HOME_USER"),"pkgdev","pkgs",pkgName), recursive=TRUE) #bug? +} + +#' Launch R unit tests (arbitrary file structure under R/tests), or display the list of test functions +#' +#' @param path Location of the package containing tests (under /R/tests) +#' @param prefix Prefix for names of the functions to be tested; leave empty to test all (default) +#' @param show Logical, TRUE to display the list of unit tests (default: FALSE) +pkgdev.rtest = function(path, prefix="", show=FALSE, cc="gcc -std=gnu99") { + path = normalizePath(path) #allow to give only pkg name (in pkg/.. folder) + .execMethod(".pkgdev.rtest", path, prefix, show, cc) +} + +#' Launch C unit tests (arbitrary file structure under src/tests), or display the list of test functions +#' +#' @param path Location of the package containing tests (under /src/tests) +#' @param prefix Prefix for names of the functions to be tested; leave empty to test all (default) +#' @param show Logical, TRUE to display the list of unit tests (default: FALSE) +pkgdev.ctest = function(path, prefix="", show=FALSE, cc="gcc -std=gnu99") { + .execMethod(".pkgdev.ctest", path, prefix, show, cc) +} + +#' "Flatten" a package: gather all sources under R/ and src/ without hierarchical file structure +#' +#' @param inPath Input path: location of the package to flatten +#' @param outPath Output path: location of the package to create [default: inPath_cran] +pkgdev.tocran = function(inPath, outPath=NULL) { + inPath = normalizePath(inPath) #allow to give only pkg name (in pkg/.. folder) + if (is.null(outPath)) outPath = paste(inPath, "_cran", sep='') + .execMethod(".pkgdev.tocran", inPath, outPath) +} + +#' Invoke R CMD check on a flat package (maybe with optional arguments, like --as-cran) +#' +#' @param opts Vector of strings arguments to pass to R CMD CHECK +pkgdev.check = function(path, opts) { + path = normalizePath(path) #allow to give only pkg name (in pkg/.. folder) + system( paste("R CMD check", opts, path, sep=' ') ) +} diff --git a/README b/README new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/README @@ -0,0 +1 @@ +TODO