--- livecd-creator.git20070718	2007-07-18 22:20:39.000000000 -0500
+++ livecd-creator.git_dmc	2007-07-18 22:20:39.000000000 -0500
@@ -144,9 +144,10 @@
         self.mounted = True
 
 class SparseExt3LoopbackMount(LoopbackMount):
-    def __init__(self, lofile, mountdir, size, fslabel, tmpdir):
+    def __init__(self, lofile, mountdir, size, container_size, fslabel, tmpdir):
         LoopbackMount.__init__(self, lofile, mountdir, fstype = "ext3")
         self.size = size
+        self.container_size = container_size
         self.fslabel = fslabel
 
     def _createSparseFile(self):
@@ -156,14 +157,15 @@
 
         # create the sparse file
         fd = os.open(self.lofile, os.O_WRONLY | os.O_CREAT)
-        off = long(self.size * 1024L * 1024L)
+        off = long(self.container_size * 1024L * 1024L)
         os.lseek(fd, off, 0)
         os.write(fd, '\x00')
         os.close(fd)
 
     def _formatFilesystem(self):
         rc = subprocess.call(["/sbin/mkfs.ext3", "-F", "-L", self.fslabel,
-                              "-m", "1", self.lofile])
+                              "-m", "1", "-b", "4096", self.lofile,
+                              "%d" % ( self.size *  1024L * 1024L / 4096L )])
         if rc != 0:
             raise MountError("Error creating ext3 filesystem")
         rc = subprocess.call(["/sbin/tune2fs", "-c0", "-i0", "-Odir_index",
@@ -383,7 +385,7 @@
         fstab.write("sysfs                   /sys                    sysfs   defaults        0 0\n")
         fstab.close()
 
-    def setup(self, image_size, base_on = None):
+    def setup(self, image_size, container_size, base_on = None):
         """setup target ext3 file system in preparation for an install"""
 
         # setup temporary build dirs
@@ -410,6 +412,7 @@
                                                     "%s/install_root"
                                                     %(self.build_dir,),
                                                     image_size,
+                                                    container_size,
                                                     self.fs_label,
                                                     self.tmpdir)
             
@@ -468,6 +471,10 @@
             b.umount()
 
         if self.instloop:
+            (xxbsize, xxfrsize, xxblocks, xxbfree,
+             xxbavail, xxfiles, xxffree, xxfavail,
+             xxflag, xxnamemax) = os.statvfs(self.build_dir + "/install_root")
+            print >> sys.stderr, "Installation target uncompressed data size is %d MB" % ( ( xxfrsize * xxblocks - xxbsize * xxbfree ) / ( 1024L * 1024L ) )
             self.instloop.cleanup()
             self.instloop = None
 
@@ -916,7 +923,96 @@
             shutil.move("%s/data/os.img" %(self.build_dir,),
                         "%s/out/ext3fs.img" %(self.build_dir,))
 
+    # cleanupDeleted removes unused data from the sparse ext3 os image file.
+    # The process involves: resize2fs-to-minimal, truncation,
+    # resize2fs-to-uncompressed-size, and finally resparsification back to
+    # container-size
+    #
+    # Anecdotally, on one particular build system, building an iso very
+    # similar to the stock f7-i686-livecd, cleanupDeleted incurred a
+    # 25% build time penalty, i.e. 24m -> 30m.
+    # about 2m was spent in figuring out the minimal resize amount
+    # about 2.5m was spent in the cp --sparse
+    # about 1.5m was spent in the resize2fs minimal
+    # the resize2fs maximal was only a few seconds
+    #
+    # The output changed from 697M to 661M.  Which matches what I got
+    # when I manually reconstructed the official f7livecd and got 700->664
+    #
+    # The 2.5m cp --sparse is somewhat silly as it changes the data size
+    # of the sparse file from 2062 to 2036, and because of compression only
+    # resulted in a final savings of a third to half a megabyte.  But hey,
+    # when you're trying to squeeze just a little more into a mass shipping
+    # CD, I think it's worth it.
+    def cleanupDeleted(self, uncompressed_size, container_size):
+        # e2fsck -f is required by resize2fs
+        subprocess.call(["/sbin/e2fsck", "-f", "os.img"],
+                        cwd="%s/data" %(self.build_dir,),
+                        env={"PWD": "%s/data" %(self.build_dir,)})
+
+        # resize2fs doesn't have any kind of minimal setting, so start
+        # with 1MB and increment by 1MB until it works.  This happens
+        # quite quickly.  Maybe figure out a better method someday.
+        FNULL = os.open('/dev/null', os.O_WRONLY)
+        newsize = 1
+        done = 0
+        while done == 0:
+            # note: might be nice to use a buffer rather than devnull
+            #       so that the final successful output can be printed.
+            resize2fs_retval = subprocess.call(["/sbin/resize2fs", "os.img",
+                                                "%dM" %(newsize,)],
+                                               cwd="%s/data" %(self.build_dir,),
+                                               env={"PWD": "%s/data" %(self.build_dir,)},
+                                               stdout=FNULL,
+                                               stderr=FNULL)
+
+            if not resize2fs_retval:
+                done = 1
+                print >> sys.stderr, "installation target minimized container size is %d MB" % newsize
+            else:
+                newsize = newsize + 1
+                if newsize > uncompressed_size:
+                    raise InstallationError("unexpected error with resize2fs in cleanup-deleted pass")
+
+        os.close(FNULL)
+
+        # truncate the unused excess portion of the container file
+        fd = os.open("%s/data/os.img" %(self.build_dir,), os.O_WRONLY )
+        os.ftruncate(fd, newsize * 1024L * 1024L)
+        os.close(fd)
+                
+        # NOTE: cp --sparse=always will shrink os.img here another 1-2%
+        # Not exactly sure why.  In compressed size, I got this result
+        # 692828160 vs 692422656 not even half a meg...  But hey.
+        try:
+            subprocess.call(["/bin/cp", "-a", "--sparse=always", "os.img",
+                             "os.img.sparse"],
+                            cwd="%s/data" %(self.build_dir,),
+                            env={"PWD": "%s/data" %(self.build_dir,)})
+            os.unlink("%s/data/os.img" %(self.build_dir,))
+            os.rename("%s/data/os.img.sparse" %(self.build_dir,),
+                      "%s/data/os.img" %(self.build_dir,))
+
+        except:
+            raise InstallationError("Error while cleaning up deleted files: %s" %(e,))
+                      
+        # resize back to uncompressed-size
+        resize2fs_retval = subprocess.call(["/sbin/resize2fs", "os.img",
+                                            "%dM" %(uncompressed_size,)],
+                                           cwd="%s/data" %(self.build_dir,),
+                                           env={"PWD": "%s/data" %(self.build_dir,)})
+        
+
+        # resparsify back to container size
+        fd = os.open("%s/data/os.img" %(self.build_dir,), os.O_WRONLY )
+        off = long(container_size * 1024L * 1024L)
+        os.lseek(fd, off, 0)
+        os.write(fd, '\x00')
+        os.close(fd)
+
+
     def package(self):
+        
         self.createSquashFS()
         self.createIso()
 
@@ -931,6 +1027,8 @@
                       [--fslabel=<label>]
                       [--skip-compression]
                       [--uncompressed-size=<size-in-MB>]
+                      [--container-size=<size-in-MB>]
+                      [--cleanup-deleted]
                       [--shell]
                       [--tmpdir=<tmpdir>]
 
@@ -944,6 +1042,10 @@
  --skip-compression  : Don't compress the image
  --prelink           : Prelink the image
  --uncompressed-size : Size of uncompressed fs in MB (default: 4096)
+ --container-size    : Size of uncompressed fs container in MB
+                           (default: same as uncompressed-size)
+ --cleanup-deleted   : Use a couple resize2fs passes to remove blocks used
+                           by files deleted during installation 
  --shell             : Start a shell in the chroot for post-configuration
  --tmpdir            : Temporary directory to use (default: /var/tmp)
 
@@ -974,6 +1076,8 @@
         self.skip_compression = False
         self.skip_prelink = True
         self.uncompressed_size = 4096
+        self.container_size = self.uncompressed_size
+        self.cleanup_deleted = False
         self.give_shell = False
         self.tmpdir = "/var/tmp"
 
@@ -983,7 +1087,10 @@
                                    ["help", "repo=", "base-on=", "package=",
                                     "exclude-package=", "fslabel=", "config=",
                                     "skip-compression", "uncompressed-size=",
-                                    "shell", "no-prelink", "prelink","tmpdir="])
+                                    "container-size=", "cleanup-deleted",
+                                    "shell", "no-prelink", "prelink",
+                                    "tmpdir="])
+
     except getopt.GetoptError, msg:
         raise Usage(msg)
 
@@ -1007,6 +1114,12 @@
         if o in ("-u", "--uncompressed-size"):
             options.uncompressed_size = int(a)
             continue
+        if o in ("--container-size"):
+            options.container_size = int(a)
+            continue
+        if o in ("--cleanup-deleted",):
+            options.cleanup_deleted = True
+            continue
         if o in ("-c", "--config"):
             options.kscfg = a
             if not os.path.isfile(options.kscfg):
@@ -1082,7 +1195,9 @@
     try:
         target.parse(options.kscfg)
 
-        target.setup(options.uncompressed_size, options.base_on)
+        target.setup(options.uncompressed_size,
+                     options.container_size,
+                     options.base_on)
 
         target.install()
 
@@ -1091,9 +1206,18 @@
             print "----------------------------------"
             target.launchShell()
 
+
         target.unmount()
 
+        # note: it might be more graceful to use something like stat and
+        #       dumpe2fs -h to have cleanupDeleted calculate uncompressed_size
+        #       and container_size rather than passing them as args here.
+        if options.cleanup_deleted:
+            target.cleanupDeleted(options.uncompressed_size,
+                                 options.container_size)
+            
         target.package()
+        
     except InstallationError, e:
         print >> sys.stderr, "Error creating Live CD : %s" % e
         target.teardown()
