initial commit
authorBenjamin Auder <benjamin.auder@math.u-psud.fr>
Wed, 21 May 2014 15:58:04 +0000 (17:58 +0200)
committerBenjamin Auder <benjamin.auder@math.u-psud.fr>
Wed, 21 May 2014 15:58:04 +0000 (17:58 +0200)
12 files changed:
DESCRIPTION [new file with mode: 0755]
NAMESPACE [new file with mode: 0644]
R/CunitTestsMacros.R [new file with mode: 0644]
R/RunitTestsMethods.R [new file with mode: 0644]
R/load.R [new file with mode: 0644]
R/runCtests.R [new file with mode: 0644]
R/runRtests.R [new file with mode: 0644]
R/setup.R [new file with mode: 0644]
R/tocran.R [new file with mode: 0644]
R/unload.R [new file with mode: 0644]
R/wrappers.R [new file with mode: 0644]
README [new file with mode: 0644]

diff --git a/DESCRIPTION b/DESCRIPTION
new file mode 100755 (executable)
index 0000000..df83ff4
--- /dev/null
@@ -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 <Benjamin.Auder@gmail.com>
+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 (file)
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 (file)
index 0000000..77872c0
--- /dev/null
@@ -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 <stdlib.h>
+#include <stdio.h>
+
+// 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 (file)
index 0000000..67c31dd
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..9bdc43b
--- /dev/null
@@ -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 <stdlib.h>
+#include <stdio.h>
+#include <time.h> // 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 (file)
index 0000000..b649ead
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..d6e4440
--- /dev/null
@@ -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 (file)
index 0000000..89d8152
--- /dev/null
@@ -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 (file)
index 0000000..2af3302
--- /dev/null
@@ -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 (file)
index 0000000..1333ed7
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+TODO