Part 2/2 of SF patch #416704: More robust freeze, by Toby Dickenson.

(With slight cosmetic improvements to shorten lines and a grammar fix
to a docstring.)

This addes -X and -E options to freeze.  From the docstring:

-X module     Like -x, except the module can never be imported by
              the frozen binary.

-E:           Freeze will fail if any modules can't be found (that
              were not excluded using -x or -X).
diff --git a/Tools/freeze/freeze.py b/Tools/freeze/freeze.py
index b534098..c347d53 100755
--- a/Tools/freeze/freeze.py
+++ b/Tools/freeze/freeze.py
@@ -42,7 +42,14 @@
 
 -h:           Print this help message.
 
--x module     Exclude the specified module.
+-x module     Exclude the specified module. It will still be imported
+              by the frozen binary if it exists on the host system.
+
+-X module     Like -x, except the module can never be imported by
+              the frozen binary.
+
+-E:           Freeze will fail if any modules can't be found (that
+              were not excluded using -x or -X).
 
 -i filename:  Include a file with additional command line options.  Used
               to prevent command lines growing beyond the capabilities of
@@ -114,10 +121,15 @@
     odir = ''
     win = sys.platform[:3] == 'win'
     replace_paths = []                  # settable with -r option
+    error_if_any_missing = 0
 
     # default the exclude list for each platform
     if win: exclude = exclude + [
-        'dos', 'dospath', 'mac', 'macpath', 'macfs', 'MACFS', 'posix', 'os2', 'ce']
+        'dos', 'dospath', 'mac', 'macpath', 'macfs', 'MACFS', 'posix',
+        'os2', 'ce', 'riscos', 'riscosenviron', 'riscospath',
+        ]
+
+    fail_import = exclude[:]
 
     # modules that are imported by the Python runtime
     implicits = ["site", "exceptions"]
@@ -129,14 +141,17 @@
     makefile = 'Makefile'
     subsystem = 'console'
 
-    # parse command line by first replacing any "-i" options with the file contents.
+    # parse command line by first replacing any "-i" options with the
+    # file contents.
     pos = 1
-    while pos < len(sys.argv)-1: # last option can not be "-i", so this ensures "pos+1" is in range!
+    while pos < len(sys.argv)-1:
+        # last option can not be "-i", so this ensures "pos+1" is in range!
         if sys.argv[pos] == '-i':
             try:
                 options = string.split(open(sys.argv[pos+1]).read())
             except IOError, why:
-                usage("File name '%s' specified with the -i option can not be read - %s" % (sys.argv[pos+1], why) )
+                usage("File name '%s' specified with the -i option "
+                      "can not be read - %s" % (sys.argv[pos+1], why) )
             # Replace the '-i' and the filename with the read params.
             sys.argv[pos:pos+2] = options
             pos = pos + len(options) - 1 # Skip the name and the included args.
@@ -144,7 +159,7 @@
 
     # Now parse the command line with the extras inserted.
     try:
-        opts, args = getopt.getopt(sys.argv[1:], 'r:a:de:hmo:p:P:qs:wx:l:')
+        opts, args = getopt.getopt(sys.argv[1:], 'r:a:dEe:hmo:p:P:qs:wX:x:l:')
     except getopt.error, msg:
         usage('getopt error: ' + str(msg))
 
@@ -175,6 +190,11 @@
             subsystem = a
         if o == '-x':
             exclude.append(a)
+        if o == '-X':
+            exclude.append(a)
+            fail_import.append(a)
+        if o == '-E':
+            error_if_any_missing = 1
         if o == '-l':
             addn_link.append(a)
         if o == '-a':
@@ -225,7 +245,9 @@
 
     # sanity check of directories and files
     check_dirs = [prefix, exec_prefix, binlib, incldir]
-    if not win: check_dirs = check_dirs + extensions # These are not directories on Windows.
+    if not win:
+        # These are not directories on Windows.
+        check_dirs = check_dirs + extensions
     for dir in check_dirs:
         if not os.path.exists(dir):
             usage('needed directory %s not found' % dir)
@@ -350,8 +372,14 @@
         print
     dict = mf.modules
 
+    if error_if_any_missing:
+        missing = mf.any_missing()
+        if missing:
+            sys.exit("There are some missing modules: %r" % missing)
+
     # generate output for frozen modules
-    files = makefreeze.makefreeze(base, dict, debug, custom_entry_point)
+    files = makefreeze.makefreeze(base, dict, debug, custom_entry_point,
+                                  fail_import)
 
     # look for unfrozen modules (builtin and of unknown origin)
     builtins = []
diff --git a/Tools/freeze/makefreeze.py b/Tools/freeze/makefreeze.py
index b7bf9fd..ac59a9c 100644
--- a/Tools/freeze/makefreeze.py
+++ b/Tools/freeze/makefreeze.py
@@ -32,7 +32,7 @@
 
 """
 
-def makefreeze(base, dict, debug=0, entry_point = None):
+def makefreeze(base, dict, debug=0, entry_point=None, fail_import=()):
     if entry_point is None: entry_point = default_entry_point
     done = []
     files = []
@@ -63,6 +63,13 @@
     outfp.write(header)
     for mod, mangled, size in done:
         outfp.write('\t{"%s", M_%s, %d},\n' % (mod, mangled, size))
+    outfp.write('\n')
+    # The following modules have a NULL code pointer, indicating
+    # that the prozen program should not search for them on the host
+    # system. Importing them will *always* raise an ImportError.
+    # The zero value size is never used.
+    for mod in fail_import:
+        outfp.write('\t{"%s", NULL, 0},\n' % (mod,))
     outfp.write(trailer)
     outfp.write(entry_point)
     outfp.close()
diff --git a/Tools/freeze/modulefinder.py b/Tools/freeze/modulefinder.py
index 015708b..924c3a4 100644
--- a/Tools/freeze/modulefinder.py
+++ b/Tools/freeze/modulefinder.py
@@ -356,8 +356,12 @@
         return m
 
     def find_module(self, name, path):
-        if name in self.excludes:
-            self.msgout(3, "find_module -> Excluded")
+        if path:
+            fullname = '.'.join(path)+'.'+name
+        else:
+            fullname = name
+        if fullname in self.excludes:
+            self.msgout(3, "find_module -> Excluded", fullname)
             raise ImportError, name
 
         if path is None:
@@ -397,6 +401,15 @@
                 mods.sort()
                 print "?", key, "from", string.join(mods, ', ')
 
+    def any_missing(self):
+        keys = self.badmodules.keys()
+        missing = []
+        for key in keys:
+            if key not in self.excludes:
+                # Missing, and its not supposed to be
+                missing.append(key)
+        return missing
+
     def replace_paths_in_code(self, co):
         new_filename = original_filename = os.path.normpath(co.co_filename)
         for f,r in self.replace_paths: