diff --git a/.gitignore b/.gitignore
index 6f97ca1afcc063e5102e7192528d2c2521213aaa..7e5228b8368ab4b1b37a208b46bf08c1d8d8f6b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
+MANIFEST
+dist
 build
 *.pyc
+*.swp
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..b0e4bc26f0c8d420970b1e5589f9f4c1082a0e36
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+include LICENSE OWNERS README
+include chkcrontab.py
+include chkcrontab_lib.py
+include setup.py
+recursive-include tests test*
diff --git a/README b/README
index 7d4d0010dcad982ba055bd09c3975e995869f728..5e9031ea57d17185df377cc3163680763b578bd5 100644
--- a/README
+++ b/README
@@ -1,3 +1,5 @@
+DESCRIPTION
+
 chkcrontab.py is a script to check crontab files like those in /etc/cron.d
 and /etc/crontab.  It tries to catch glaring errors and warn on suspect
 lines in a crontab file.  Some valid lines will generate warnings.
@@ -9,7 +11,12 @@ Errors will cause a non-zero exit code.  Warnings alone will not.
 
 To see sample output for a bad crontab, run the following:
 
-  ./scripts/chkcrontab.py ./tests/test_crontab
+  ./chkcrontab.py ./tests/test_crontab
 
 See the ./tests/test_crontab.disable crontab for how to disable warnings
 and errors.
+
+CONTRIBUTIONS
+
+Please add unit tests for new features or bug fixes.  To run all
+the unitests run ./setup test.
diff --git a/setup.py b/setup.py
index da5331c4757f15bcb44e1958b15a0e5037131b10..a1bc11d102e4f64e9430ed027b20d6d69831a29f 100755
--- a/setup.py
+++ b/setup.py
@@ -18,9 +18,109 @@
 This installs the chkcrontab command and the crontab.check module.
 """
 
+import os
+import sys
+from distutils import log
 from distutils.core import setup
+from distutils.core import Command
+if sys.version_info < (2, 7):
+  import unittest2 as unittest
+else:
+  import unittest
+
+BASE_DIR = os.path.dirname(globals().get('__file__', os.getcwd()))
+
+
+class TestCommand(Command):
+  description = 'Runs all available tests.'
+  user_options = [ ]
+
+  def initialize_options(self):
+    pass
+
+  def finalize_options(self):
+    pass
+
+  def run(self):
+    test_dir = os.path.join(BASE_DIR, 'tests')
+
+    tests = unittest.TestLoader().discover(test_dir)
+    runner = unittest.TextTestRunner(verbosity=2)
+    result = runner.run(tests)
+    if not result.wasSuccessful():
+      sys.exit(1)
+
+class CleanCommand(Command):
+  description = 'Remove all generated files.'
+  user_options = [ ]
+
+  def initialize_options(self):
+    pass
+
+  def finalize_options(self):
+    pass
+
+  def run(self):
+    # Configure for this project.
+    suffixes2del = [ 'MANIFEST', '.pyc' ]
+    dirs2del = [ './build', './dist' ]
+    dirs2ign = [ './.git' ]
+    # End config.
+    doomed = [ ]
+    # Change to base dir.
+    os.chdir(BASE_DIR)
+    for root, dirs, files in os.walk('.'):
+      # Handle root dirs.
+      if root in dirs2ign:
+        continue
+      if root in dirs2del:
+        doomed.append(root)
+      # Handle files.
+      for f in files:
+        accused = os.path.join(root, f)
+        for suffix in suffixes2del:
+          if f.endswith(suffix):
+            doomed.append(accused)
+            break
+        if accused not in doomed:
+          for d2del in dirs2del:
+            if accused.startswith(d2del):
+              doomed.append(accused)
+              break
+      # Handle dirs.
+      for d in dirs:
+        accused = os.path.join(root, d)
+        for d2ign in dirs2ign:
+          if accused.startswith(d2ign):
+            dirs.remove(d)
+            break
+        if d in dirs:
+          for d2del in dirs2del:
+            if accused.startswith(d2del):
+              doomed.append(accused)
+              break
+    # Probably not required, but just to be safe.
+    for accused in doomed:
+      for d2ign in dirs2ign:
+        if accused.startswith(d2ign):
+          doomed.remove(accused)
+          break
+    doomed.sort(reverse=True)
+    for accused in doomed:
+      log.info('removing "%s"', os.path.normpath(accused))
+      if not self.dry_run:
+        try:
+          os.unlink(accused)
+        except:
+          try:
+            os.rmdir(accused)
+          except:
+            log.warn('unable to remove "%s"', os.path.normpath(accused))
 
 setup(
+    cmdclass={'test': TestCommand,
+              'dist_clean': CleanCommand
+             },
     name='chkcrontab',
     version='1.0',
     url='http://code.google.com/p/chkcrontab',
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/tests/runall.py b/tests/runall.py
deleted file mode 100755
index 65fca039fb51c4e558d922f3d61ea6ec2b69ad28..0000000000000000000000000000000000000000
--- a/tests/runall.py
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/python
-#
-# Copyright 2011 Google Inc. All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Run ***ALL*** the testorz!!!"""
-
-import os
-import sys
-if sys.version_info < (2, 7):
-  import unittest2 as unittest
-else:
-  import unittest
-
-BASE_PATH = os.path.dirname(__file__)
-sys.path.insert(0, os.path.join(BASE_PATH, '..'))
-
-if __name__ == '__main__':
-  test_dir = os.path.dirname(globals().get('__file__', os.getcwd()))
-
-  tests = unittest.TestLoader().discover(test_dir)
-  runner = unittest.TextTestRunner(verbosity=2)
-  result = runner.run(tests)
-  if not result.wasSuccessful():
-    sys.exit(1)