Search code examples
pythonzipscons

How to make SCons not include the base dir in zip files?


SCons provides a Zip builder to produce zip files from groups of files. For example, suppose we have a folder foo that looks like this:

foo/
foo/blah.txt

and we create the zip file foo.zip from a folder foo:

env.Zip('foo.zip', 'foo/')

This produces a zip file:

$ unzip -l foo.zip
Archive:  foo.zip
  foo/
  foo/foo.txt

However, suppose we are using a VariantDir of bar, which contains foo:

bar/
bar/foo/
bar/foo/foo.txt

Because we are in a VariantDir, we still use the same command to create the zip file, even though it has slightly different effects:

env.Zip('foo.zip', 'foo/')

This produces the zip file:

$ unzip -l bar/foo.zip
Archive:  bar/foo.zip
  bar/foo/
  bar/foo/foo.txt

The problem is extra bar/ prefix for each of the files within the zip. If this was not SCons, the simple solution would be to cd into bar and call zip from within there with something like cd bar; zip -r foo.zip foo/. However, this is weird/difficult with SCons, and at any rate seems very un-SCons-like. Is there a better solution?


Solution

  • You can create a SCons Builder which accomplishes this task. We can use the standard Python zipfile to make the zip files. We take advantage of zipfile.write, which allows us to specify a file to add, as well as what it should be called within the zip:

    zf.write('foo/bar', 'bar') # save foo/bar as bar
    

    To get the right paths, we use os.path.relpath with the path of the base file to find the path to the overall file.

    Finally, we use os.walk to walk through contents of directories that we want to add, and call the previous two functions to add them, correctly, to the final zip.

    import os.path
    import zipfile
    
    def zipbetter(target, source, env):
        # Open the zip file with appending, so multiple calls will add more files
        zf = zipfile.ZipFile(str(target[0]), 'a', zipfile.ZIP_DEFLATED)
        for s in source:
            # Find the path of the base file
            basedir = os.path.dirname(str(s))
            if s.isdir():
                # If the source is a directory, walk through its files
                for dirpath, dirnames, filenames in os.walk(str(s)):
                    for fname in filenames:
                        path = os.path.join(dirpath, fname)
                        if os.path.isfile(path):
                            # If this is a file, write it with its relative path
                            zf.write(path, os.path.relpath(path, basedir))
            else:
                # Otherwise, just write it to the file
                flatname = os.path.basename(str(s))
                zf.write(str(s), flatname)
        zf.close()
    
    # Make a builder using the zipbetter function, that takes SCons files
    zipbetter_bld = Builder(action = zipbetter,
                            target_factory = SCons.Node.FS.default_fs.Entry,
                            source_factory = SCons.Node.FS.default_fs.Entry)
    
    # Add the builder to the environment
    env.Append(BUILDERS = {'ZipBetter' : zipbetter_bld})
    

    Call it just like the normal SCons Zip:

    env.ZipBetter('foo.zip', 'foo/')