Welcome, guest | Sign In | My Account | Store | Cart

This is a command-line tool for making self-extracting file archives in Python.

Python, 88 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
"""Command-line tool for making self-extracting Python file.

Call this program from your command line with one argument:
  (1) the file that you want to pack and compress
  (2) the output will be a file with a pyw ending
The output can run on Windows where Python is installed."""

################################################################################

import sys
import os.path
import bz2
import zlib
import base64

################################################################################

def main():
    "Extract the command-line arguments and run the packer."
    try:
        pack(sys.argv[1])
    except (IndexError, AssertionError):
        print('Usage: {} <filename>'.format(os.path.basename(sys.argv[0])))

def pack(path):
    "Get the source, compress it, and create a packed file."
    data = read_file(path)
    builder, data = optimize(data)
    with open(os.path.splitext(path)[0] + '.pyw', 'w') as file:
        builder(os.path.basename(path), base64.b64encode(data), file)

def read_file(path):
    "Read the entire file content from path in binary mode."
    assert os.path.isfile(path)
    with open(path, 'rb') as file:
        return file.read()

def optimize(data):
    "Compress the data and select the best method to write."
    bz2_data = bz2.compress(data, 9)
    zlib_data = zlib.compress(data, 9)
    sizes = tuple(map(len, (data, bz2_data, zlib_data)))
    smallest = sizes.index(min(sizes))
    if smallest == 1:
        return build_bz2_extractor, bz2_data
    if smallest == 2:
        return build_zlib_extractor, zlib_data
    return build_b64_extractor, data

################################################################################

def build_bz2_extractor(filename, data, file):
    "Write a Python program that uses bz2 data compression."
    print("import base64, bz2, os", file=file)
    print("data =", data, file=file)
    print("with open({!r}, 'wb') as file:".format(filename), file=file)
    print("    file.write(bz2.decompress(base64.b64decode(data)))", file=file)
    print("os.startfile({!r})".format(filename), file=file)

def build_zlib_extractor(filename, data, file):
    "Pack data into a self-extractor with zlib compression."
    print("import base64, zlib, os", file=file)
    print("data =", data, file=file)
    print("with open({!r}, 'wb') as file:".format(filename), file=file)
    print("    file.write(zlib.decompress(base64.b64decode(data)))", file=file)
    print("os.startfile({!r})".format(filename), file=file)

def build_b64_extractor(filename, data, file):
    "Create a Python file that may not utilize compression."
    print("import base64, os", file=file)
    print("data =", data, file=file)
    print("with open({!r}, 'wb') as file:".format(filename), file=file)
    print("    file.write(base64.b64decode(data))", file=file)
    print("os.startfile({!r})".format(filename), file=file)

################################################################################

if __name__ == '__main__':
    main()

# Small Program Version

# import bz2,base64 as a,os.path as b,sys,zlib;c=sys.argv[1]
# with open(c,'rb') as d:d=d.read();e,f=bz2.compress(d),zlib.compress(d,9);g=list(map(len,(d,e,f)));g,h,i,j,k,l=g.index(min(g)),'import base64 as a,os','\nwith open({0!r},"wb") as b:b.write(','.decompress(','a.b64decode({1}))',';os.startfile({0!r})'
# if g==1:d,e=e,h+',bz2'+i+'bz2'+j+k+')'+l
# elif g==2:d,e=f,h+',zlib'+i+'zlib'+j+k+')'+l
# else:e=h+i+k+l
# with open(b.splitext(c)[0]+'.pyw','w') as f:f.write(e.format(b.basename(c),a.b64encode(d)))

7 comments

Stephen Chappell (author) 13 years, 3 months ago  # | flag

This is the same program after being run on itself. The last line has been slightly modified.

import base64, zlib, os
data = b'eNrNVk1v1DAQva+0/2EIh01EGtGqqgSiSKhCiBtCIA4IRd7E6VpN7Mh2SLeI/86MP7LZso\
UeqGhOcTKer/fe2EmSXKiuY7I+aoXkYJVqoVEaOnYl5CUY3jZH/NpqVllaf9jajZLQiJYXy8VyccHaF\
uxGGOi1utSsg0arDrZq0FB5x+Acj8JuwI4KmL4cOi6teblcAKTHGW7nziG+MEtbYWTSYirQs+oKyAW6\
6jU3xm058Vsk6zioZrd9vrPSnFm0UeNy8QkN1GD7wULFJOhBApbwRchajQbGDdc81jUyA0Iai1Xxuki\
ShGp8+o8f8im6XmkLZmumd2WKntnNtF7fnEzvN61Y734ww89OHyqzmjcIvpBp5gACSN56+F2jqzlZJi\
gdRtRXMiHQuC4Sv9vqbfBDD/1LsegCt37/evwt87/4dcV7C+l7WfPrt1orncMbY7i2Qkm3zuZOtJA2X\
X027JK/hB8/4RXBT3R4vSqQux2zaWhmQb2iP7ugz79lWRYLdfmQ4VTsO+4LNcjgiucT80DY3FPRM4v5\
OmsvhVBszSyDc0CLuqTv3rX/1wyyonLyaKV6Kzpxw1NaByOnEtVzORVg+lZYFKB3hdnDM1gV/XZc5bA\
aVxkgYynUrD8x0u9NcD7yQKBifXbKZaXqkEHu/EytuVVE7M9H/OwahLgLHZRXKWlx7bVP5igiWAvJ9B\
Y6DBD7wxymkemFMLebtKufPmKFen2wRM3toMMYokTTKe1bbY1pX0QYKXUHAGGJ040HYq+5sdBxnAI1z\
Y9RY9dj2qjEMoCGr0WkhIuQw4uQO2k0mtH7XXYGkzNoY4cei+9YnyJ5cwhWMVa+8+f46nZ2NJcwz3Pv\
pBCkl7RDsbp1tBPNzPQcjn9vnCdxScHCcCfJxdgHvZzc6cUlOnMzJe433Ap5djq39WYPN8kOFZrGceH\
DB95Hpnwh5FHe4USIx5o7mwaDyKEvz6CIL0otMsWPpmRvULu+5kj6xEc6DzKb2XvWJPN8DljtxPHjif\
5J8p+LI4mjL1aX3enIDQmSjmN5Spyu+cTW3XSgj3E6ZNnd6aOe8cjU1qmZcsv+lsweOPv8+TM6H+hG4\
NqFkfEysXdBwWuL6xE5vD86ZP2Y4XGz5H/isyfZP8NzEQ/H2T3RS6djW7yNoYSsaHFW3R+eR6ybw0D8\
Wxwe5PLZQFlStLKkyb4qS7rwleUqwOivf8vFL5PHm/k='
with open('pack.py', 'wb') as file:
    file.write(zlib.decompress(base64.b64decode(data)))
os.system('cmd /k pack.py')
Stephen Chappell (author) 13 years, 3 months ago  # | flag

If you want a minimized version of the archiver that is obfuscated and lacks error correction, here is the program run on itself.

import base64,zlib,os;data=b'eNqlks1uwyAQhO+R8g7uCVARSqKoUm3xJGkOi9k0SDZGQOv8qO9ewKnSKu0lvXCYZWc+jdb0bvCxCsf\
AhyAcxD1XpxU/dUZxBQGf1g3INBXgX983y+18Npq4rwaHlgInXhFWQahUraQSHkFT1rRcy2Qi2qF3HkOgij+zYvlTalDGN9ch7cHRDi1Patp\
lLE9QGKvxQHtjKTI2n5ldhVIu6/RHtpwQYib0ibJQD6HREEGeFw/+48VeSc/LJHAyXmh3psM6P2L0JiLNsBqvbMVRqKd1FjXSbJqpUkMhgo9\
5dfJkiWM+w26CWxU4fQtX6ryXrhT3P7yAdStvqO6u63eAP/OvxpcTE8F1yegQKbDNYvtIhDuOJAVOebrWl6RW7AbfQ/wWibZEKvZ1ryLPLPR\
IcwmfF63uLQ=='
with open('pack.py','wb') as file:file.write(zlib.decompress(base64.b64decode(data)));os.startfile('pack.py')
Stephen Chappell (author) 13 years, 3 months ago  # | flag

This is may not be Code Golf, but trying to get the program shorter has been a lot of fun:

import base64,zlib,os
with open('pack.py','wb') as a:a.write(zlib.decompress(base64.b64decode(b'eNql0\
t9qwyAUBvD7Qt4hu1KZSFNKYSk+SdeLYzxdhcSIuqV/2LvPmLIwSqGwW835vp+HmM71PpbhHHgfhIN4\
5Oqy4pfWKK4g4Ga9BZluBfiPr121LxaDiceyd2gpcOIVYSWEUtVKKuERNGXbhmuZQkTTd85jCFTxN5Y\
j/x5tUcZP1yLtwNEWLU+naZax8QaFsRpPtDOWImPFwhxKlLKq0zey4YQQM9EnZVb34d3OvOvyxX9zMt\
yIUIMYvIlIR5vGmZIDhNqsx0ON9FqlwVGRNhIi+HgwCZnjWOotFthOmFXG6HtMXt+TmryXf3IC1o28U\
zy7jgeFD/vmzNsvI4JrU9IpUmC75f6VCHceSOqaqnStb1WNOPS+g/g7OHZb6DAN8tmBNjtUevQPGSLb\
rA==')));os.startfile('pack.py')
Stephen Chappell (author) 13 years, 3 months ago  # | flag

The last comment's program takes 664 bytes decompressed and 552 bytes compressed (without the backslash characters). In trying to compress and obfuscate the program even more, this 501 byte program was the result:

import bz2,base64 as a,os.path as b,sys,zlib;c=sys.argv[1]
with open(c,'rb') as d:d=d.read();e,f=bz2.compress(d),zlib.compress(d,9);g=list(map(len,(d,e,f)));g,h,i,j,k=g.index(min(g)),'import base64,os',"\nwith open({0!r},'wb') as a:a.write(",'base64.b64decode({1}))',';os.startfile({0!r})'
if g==1:d,e=e,h+',bz2'+i+'bz2.decompress('+j+')'+k
elif g==2:d,e=f,h+',zlib'+i+'zlib.decompress('+j+')'+k
else:e=h+i+j+k
with open(b.splitext(c)[0]+'.pyw','w') as f:f.write(e.format(b.basename(c),a.b64encode(d)))
Stephen Chappell (author) 13 years, 3 months ago  # | flag

In an attempt to compress and obfuscate the archiver a little more, here is a 498 byte version.

import bz2,base64 as a,os.path as b,sys,zlib;c=sys.argv[1]
with open(c,'rb') as d:d=d.read();e,f=bz2.compress(d),zlib.compress(d,9);g=list(map(len,(d,e,f)));g,h,i,j,k,l=g.index(min(g)),'import base64 as a,os','\nwith open({0!r},"wb") as b:b.write(','.decompress(','a.b64decode({1}))',';os.startfile({0!r})'
if g==1:d,e=e,h+',bz2'+i+'bz2'+j+k+')'+l
elif g==2:d,e=f,h+',zlib'+i+'zlib'+j+k+')'+l
else:e=h+i+k+l
with open(b.splitext(c)[0]+'.pyw','w') as f:f.write(e.format(b.basename(c),a.b64encode(d)))
AndrĂ© Berg 13 years, 3 months ago  # | flag

.pyw self extraction doesn't work on OS X and Linux, because of os.startfile.

>>> 
Traceback (most recent call last):
  File "/Users/andre/test.pyw", line 5, in <module>
    os.startfile('test.py')
AttributeError: 'module' object has no attribute 'startfile'

Nevertheless an interesting idea, so thanks for the recipe.

Stephen Chappell (author) 13 years, 3 months ago  # | flag

Thanks for pointing that out to other that might not about this limitation!

That is why in documentation at the top, Windows is specifically mentioned.