diff --git a/module/VuFindTheme/src/VuFindTheme/AbstractThemeUtility.php b/module/VuFindTheme/src/VuFindTheme/AbstractThemeUtility.php
new file mode 100644
index 0000000000000000000000000000000000000000..d0e1e70a4ca4eea18642935f43fc6ce418fe1f14
--- /dev/null
+++ b/module/VuFindTheme/src/VuFindTheme/AbstractThemeUtility.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * Abstract base class to hold shared logic for theme utilities.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2017.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Theme
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+namespace VuFindTheme;
+
+/**
+ * Abstract base class to hold shared logic for theme utilities.
+ *
+ * @category VuFind
+ * @package  Theme
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+abstract class AbstractThemeUtility
+{
+    /**
+     * Theme info object
+     *
+     * @var ThemeInfo
+     */
+    protected $info;
+
+    /**
+     * Last error message
+     *
+     * @var string
+     */
+    protected $lastError = null;
+
+    /**
+     * Constructor
+     *
+     * @param ThemeInfo $info Theme info object
+     */
+    public function __construct(ThemeInfo $info)
+    {
+        $this->info = $info;
+    }
+
+    /**
+     * Get last error message.
+     *
+     * @return string
+     */
+    public function getLastError()
+    {
+        return $this->lastError;
+    }
+
+    /**
+     * Copy the contents of $src into $dest if no matching files already exist.
+     *
+     * @param string $src  Source directory
+     * @param string $dest Target directory
+     *
+     * @return bool
+     */
+    protected function copyDir($src, $dest)
+    {
+        if (!is_dir($dest)) {
+            if (!mkdir($dest)) {
+                return $this->setLastError("Cannot create $dest");
+            }
+        }
+        $dir = opendir($src);
+        while ($current = readdir($dir)) {
+            if ($current === '.' || $current === '..') {
+                continue;
+            }
+            if (is_dir("$src/$current")) {
+                if (!$this->copyDir("$src/$current", "$dest/$current")) {
+                    return false;
+                }
+            } else if (!file_exists("$dest/$current")
+                && !copy("$src/$current", "$dest/$current")
+            ) {
+                return $this->setLastError(
+                    "Cannot copy $src/$current to $dest/$current."
+                );
+            }
+        }
+        closedir($dir);
+        return true;
+    }
+
+    /**
+     * Recursively delete a directory and its contents.
+     *
+     * @param string $path Directory to delete.
+     *
+     * @return bool
+     */
+    protected function deleteDir($path)
+    {
+        $dir = opendir($path);
+        while ($current = readdir($dir)) {
+            if ($current === '.' || $current === '..') {
+                continue;
+            }
+            if (is_dir("$path/$current")) {
+                if (!$this->deleteDir("$path/$current")) {
+                    return false;
+                }
+            } else if (!unlink("$path/$current")) {
+                return $this->setLastError("Cannot delete $path/$current");
+            }
+        }
+        closedir($dir);
+        return rmdir($path);
+    }
+
+    /**
+     * Set last error message and return a boolean false.
+     *
+     * @param string $error Error message.
+     *
+     * @return bool
+     */
+    protected function setLastError($error)
+    {
+        $this->lastError = $error;
+        return false;
+    }
+}
diff --git a/module/VuFindTheme/src/VuFindTheme/ThemeCompiler.php b/module/VuFindTheme/src/VuFindTheme/ThemeCompiler.php
index 55ceed5fdcb7896bf48b5543b815ee0fb8fdf74c..89d3d2b497a01f717fd31e643b7b7848aa38fef5 100644
--- a/module/VuFindTheme/src/VuFindTheme/ThemeCompiler.php
+++ b/module/VuFindTheme/src/VuFindTheme/ThemeCompiler.php
@@ -36,32 +36,8 @@ namespace VuFindTheme;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org Main Site
  */
-class ThemeCompiler
+class ThemeCompiler extends AbstractThemeUtility
 {
-    /**
-     * Theme info object
-     *
-     * @var ThemeInfo
-     */
-    protected $info;
-
-    /**
-     * Last error message
-     *
-     * @var string
-     */
-    protected $lastError = null;
-
-    /**
-     * Constructor
-     *
-     * @param ThemeInfo $info Theme info object
-     */
-    public function __construct(ThemeInfo $info)
-    {
-        $this->info = $info;
-    }
-
     /**
      * Compile from $source theme into $target theme.
      *
@@ -114,16 +90,6 @@ class ThemeCompiler
         return true;
     }
 
-    /**
-     * Get last error message.
-     *
-     * @return string
-     */
-    public function getLastError()
-    {
-        return $this->lastError;
-    }
-
     /**
      * Remove a theme directory (used for cleanup in testing).
      *
@@ -136,68 +102,6 @@ class ThemeCompiler
         return $this->deleteDir($this->info->getBaseDir() . '/' . $theme);
     }
 
-    /**
-     * Copy the contents of $src into $dest if no matching files already exist.
-     *
-     * @param string $src  Source directory
-     * @param string $dest Target directory
-     *
-     * @return bool
-     */
-    protected function copyDir($src, $dest)
-    {
-        if (!is_dir($dest)) {
-            if (!mkdir($dest)) {
-                return $this->setLastError("Cannot create $dest");
-            }
-        }
-        $dir = opendir($src);
-        while ($current = readdir($dir)) {
-            if ($current === '.' || $current === '..') {
-                continue;
-            }
-            if (is_dir("$src/$current")) {
-                if (!$this->copyDir("$src/$current", "$dest/$current")) {
-                    return false;
-                }
-            } else if (!file_exists("$dest/$current")
-                && !copy("$src/$current", "$dest/$current")
-            ) {
-                return $this->setLastError(
-                    "Cannot copy $src/$current to $dest/$current."
-                );
-            }
-        }
-        closedir($dir);
-        return true;
-    }
-
-    /**
-     * Recursively delete a directory and its contents.
-     *
-     * @param string $path Directory to delete.
-     *
-     * @return bool
-     */
-    protected function deleteDir($path)
-    {
-        $dir = opendir($path);
-        while ($current = readdir($dir)) {
-            if ($current === '.' || $current === '..') {
-                continue;
-            }
-            if (is_dir("$path/$current")) {
-                if (!$this->deleteDir("$path/$current")) {
-                    return false;
-                }
-            } else if (!unlink("$path/$current")) {
-                return $this->setLastError("Cannot delete $path/$current");
-            }
-        }
-        closedir($dir);
-        return rmdir($path);
-    }
-
     /**
      * Merge configurations from $src into $dest; return the result.
      *
@@ -233,17 +137,4 @@ class ThemeCompiler
         }
         return $dest;
     }
-
-    /**
-     * Set last error message and return a boolean false.
-     *
-     * @param string $error Error message.
-     *
-     * @return bool
-     */
-    protected function setLastError($error)
-    {
-        $this->lastError = $error;
-        return false;
-    }
 }
diff --git a/module/VuFindTheme/src/VuFindTheme/ThemeGenerator.php b/module/VuFindTheme/src/VuFindTheme/ThemeGenerator.php
index 78d40101e261fe7320d6e071453db886eb376959..32d01e35004f806ee3db3551e8120819cc6811ea 100644
--- a/module/VuFindTheme/src/VuFindTheme/ThemeGenerator.php
+++ b/module/VuFindTheme/src/VuFindTheme/ThemeGenerator.php
@@ -42,32 +42,8 @@ use Zend\Console\Console;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org Main Site
  */
-class ThemeGenerator
+class ThemeGenerator extends AbstractThemeUtility
 {
-    /**
-     * Theme info object
-     *
-     * @var ThemeInfo
-     */
-    protected $info;
-
-    /**
-     * Last error message
-     *
-     * @var string
-     */
-    protected $lastError = null;
-
-    /**
-     * Constructor
-     *
-     * @param ThemeInfo $info Theme info object
-     */
-    public function __construct(ThemeInfo $info)
-    {
-        $this->info = $info;
-    }
-
     /**
      * Generate a new theme from a template.
      *
@@ -84,15 +60,12 @@ class ThemeGenerator
             return $this->setLastError('Theme "' . $name . '" already exists');
         }
         Console::writeLine('Creating new theme: "' . $name . '"');
-        $source = $this->getAbsolutePath($baseDir . $themeTemplate);
-        $dest = $this->getAbsolutePath($baseDir . $name);
+        $source = $baseDir . $themeTemplate;
+        $dest = $baseDir . $name;
         Console::writeLine("\tCopying $themeTemplate");
         Console::writeLine("\t\tFrom: " . $source);
         Console::writeLine("\t\tTo: " . $dest);
-        if (!$this->copyDirectory($source, $dest)) {
-            return $this->setLastError("Copy failed.");
-        }
-        return true;
+        return $this->copyDir($source, $dest);
     }
 
     /**
@@ -108,6 +81,10 @@ class ThemeGenerator
     {
         // Enable theme
         $configPath = ConfigLocator::getLocalConfigPath('config.ini', null, true);
+        if (!file_exists($configPath)) {
+            return $this
+                ->setLastError("Expected configuration file missing: $configPath");
+        }
         Console::writeLine("\tUpdating $configPath...");
         Console::writeLine("\t\t[Site] > theme = $name");
         $writer = new ConfigWriter($configPath);
@@ -160,98 +137,4 @@ class ThemeGenerator
         }
         return true;
     }
-
-    /**
-     * Get last error message.
-     *
-     * @return string
-     */
-    public function getLastError()
-    {
-        return $this->lastError;
-    }
-
-    /**
-     * Copies contents from $source to $dest
-     *
-     * @param string $source full path to source directory
-     * @param string $dest   full path to copy destination
-     *
-     * @return boolean true on success false otherwise
-     */
-    protected function copyDirectory($source, $dest)
-    {
-        $sourceHandle = opendir($source);
-        if (!file_exists($dest)) {
-            mkdir($dest, 0755);
-        }
-
-        if (!$sourceHandle) {
-            return false;
-        }
-
-        $success = true;
-        while ($file = readdir($sourceHandle)) {
-            if ($file == '.' || $file == '..') {
-                continue;
-            }
-
-            if (is_dir($source . '/' . $file)) {
-                if (!file_exists($dest . '/' . $file)) {
-                    mkdir($dest . '/' . $file, 0755);
-                }
-                if (!$this->copyDirectory("$source/$file", "$dest/$file")) {
-                    $success = false;
-                    break;
-                }
-            } else {
-                copy($source . '/' . $file, $dest . '/' . $file);
-            }
-        }
-        closedir($sourceHandle);
-
-        return $success;
-    }
-
-    /**
-     * Removes // and /./ in paths and collapses /../
-     * Same as realpath, but doesn't check for file existence
-     *
-     * @param string $path full path to condense
-     *
-     * @return string
-     */
-    protected function getAbsolutePath($path)
-    {
-        $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
-        $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
-        $absolutes = [];
-        foreach ($parts as $part) {
-            if ('.' == $part) {
-                continue;
-            }
-            if ('..' == $part) {
-                array_pop($absolutes);
-            } else {
-                $absolutes[] = $part;
-            }
-        }
-        if (substr($path, 0, 1) === DIRECTORY_SEPARATOR) {
-            return DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $absolutes);
-        }
-        return implode(DIRECTORY_SEPARATOR, $absolutes);
-    }
-
-    /**
-     * Set last error message and return a boolean false.
-     *
-     * @param string $error Error message.
-     *
-     * @return bool
-     */
-    protected function setLastError($error)
-    {
-        $this->lastError = $error;
-        return false;
-    }
 }