3
\$\begingroup\$

I wrote a script that updates some library files for the game framework libgdx by grabbing the latest nightly build .zip file from a server and extracting the contents to the appropriate locations.

#!/usr/bin/python
__appname__ = 'libgdx_library_updater'
__version__ = "0.1"
__author__ = "Jon Renner <[email protected]>"
__url__ = "http://github.com/jrenner/libgdx-updater"
__licence__ = "MIT"
import os, time, sys, urllib2, re, datetime, tempfile, zipfile, argparse
# error handling functions and utils
def fatal_error(msg):
 print "ERROR: %s" % msg
 sys.exit(1)
def warning_error(msg):
 print "WARNING: %s" % msg
 if not FORCE:
 answer = confirm("abort? (Y/n): ")
 if answer in YES:
 fatal_error("USER QUIT") 
def confirm(msg):
 answer = raw_input(msg)
 return answer.lower()
def human_time(t):
 minutes = t / 60
 seconds = t % 60
 return "%.0fm %.1fs" % (minutes, seconds) 
# constants
YES = ['y', 'ye', 'yes', '']
# for finding the time of the latest nightly build from the web page html
DATE_RE = r"[0-9]{1,2}-[A-Za-z]{3,4}-[0-9]{4}\s[0-9]+:[0-9]+"
REMOTE_DATE_FORMAT = "%d-%b-%Y %H:%M"
SUPPORTED_PLATFORMS = ['android', 'desktop', 'gwt']
CORE_LIBS = ["gdx.jar",
 "gdx-sources.jar"]
DESKTOP_LIBS = ["gdx-backend-lwjgl.jar",
 "gdx-backend-lwjgl-natives.jar",
 "gdx-natives.jar"] 
ANDROID_LIBS = ["gdx-backend-android.jar",
 "armeabi/libgdx.so",
 "armeabi/libandroidgl20.so",
 "armeabi-v7a/libgdx.so",
 "armeabi-v7a/libandroidgl20.so"]
GWT_LIBS = ["gdx-backend-gwt.jar"] 
# parse arguments
EPILOGUE_TEXT = "%s\n%s" % (__author__, __url__) + "\nUSE AT YOUR OWN RISK!"
parser = argparse.ArgumentParser(description='LibGDX Library Updater %s' % __version__, epilog=EPILOGUE_TEXT)
parser.add_argument('-d', '--directory', help='set the libgdx project/workspace directory', default=os.getcwd())
parser.add_argument('-i', '--interactive', action='store_true', help='ask for confirmation for every file', default=False)
parser.add_argument('-f', '--force-update', action='store_true', help='no confirmations, just update without checking nightly\'s datetime', default=False)
parser.add_argument('-a', '--archive', help='specify libgdx zip file to use for update', default=None)
args = parser.parse_args()
PROJECT_DIR = args.directory
INTERACTIVE = args.interactive
FORCE = args.force_update
ARCHIVE = args.archive
# mutually exclusive
if FORCE:
 INTERACTIVE = False
# check the time of the latest archive on the nightlies server
def get_remote_archive_mtime():
 index_page = urllib2.urlopen("http://libgdx.badlogicgames.com/nightlies/")
 contents = index_page.read()
 print "-- OK --"
 # regex for filename
 regex = r"libgdx-nightly-latest\.zip"
 # add regex for anything followed by the nighlty html time format
 regex += r".*%s" % DATE_RE
 try:
 result = re.findall(regex, contents)[0]
 except IndexError as e:
 print "REGEX ERROR: failed to find '%s' in:\n%s" % (regex, contents)
 fatal_error("regex failure to match")
 try:
 mtime = re.findall(DATE_RE, result)[0]
 except IndexError as e:
 print "REGEX ERROR: failed to find datetime in: %s" % result
 fatal_error("regex failure to match")
 dtime = datetime.datetime.strptime(mtime, REMOTE_DATE_FORMAT)
 return dtime
# downloads and returns a temporary file contained the latest nightly archive
def download_libgdx_zip():
 libgdx = tempfile.TemporaryFile()
 url = "http://libgdx.badlogicgames.com/nightlies/libgdx-nightly-latest.zip"
 # testing url - don't hammer badlogic server, host the file on localhost instead
 # url = "http://localhost/libgdx-nightly-latest.zip"
 resp = urllib2.urlopen(url)
 print "downloading file: %s" % url
 total_size = resp.info().getheader('Content-Length').strip()
 total_size = int(total_size) 
 # base 10 SI units - following Ubuntu policy because it makes sense - https://wiki.ubuntu.com/UnitsPolicy
 total_size_megabytes = total_size / 1000000.0
 bytes_read = 0
 chunk_size = 10000 # 10kB per chunk
 while True: 
 chunk = resp.read(chunk_size)
 libgdx.write(chunk)
 bytes_read += len(chunk) 
 bytes_read_megabytes = bytes_read / 1000000.0
 percent = (bytes_read / float(total_size)) * 100
 sys.stdout.write("\rprogress: {:>8}{:.2f} / {:.2f} mB ({:.0f}% complete)".format(
 "", bytes_read_megabytes, total_size_megabytes, percent))
 sys.stdout.flush()
 if bytes_read >= total_size:
 print "finished download"
 break
 return libgdx
def update_files(libs, locations, archive): 
 for lib in libs: 
 if lib in archive.namelist(): 
 if INTERACTIVE:
 answer = confirm("overwrite %s? (Y/n): " % lib)
 if answer not in YES: 
 print "skipped: %s" % lib 
 continue
 with archive.open(lib, "r") as fin:
 filename = os.path.basename(lib)
 final_path = os.path.join(locations[lib], filename)
 with open(final_path, "w") as fout:
 fout.write(fin.read()) 
 print "extracted to %s" % final_path
def run_core(locations, archive):
 title("CORE")
 update_files(CORE_LIBS, locations, archive)
def run_android(locations, archive):
 title("ANDROID")
 update_files(ANDROID_LIBS, locations, archive) 
def run_desktop(locations, archive):
 title("DESKTOP")
 update_files(DESKTOP_LIBS, locations, archive) 
def run_gwt(locations, archive):
 title("GWT")
 update_files(GWT_LIBS, locations, archive)
def search_for_lib_locations(directory): 
 platforms = []
 search_list = CORE_LIBS + DESKTOP_LIBS + ANDROID_LIBS
 locations = {} 
 for element in search_list:
 locations[element] = None
 for (this_dir, dirs, files) in os.walk(directory): 
 for element in search_list:
 split_path = os.path.split(element)
 path = os.path.split(split_path[0])[-1]
 filename = split_path[1]
 for f in files:
 match = False
 if filename == f:
 f_dir = os.path.split(this_dir)[-1] 
 if path == "":
 match = True
 else:
 if path == f_dir:
 match = True
 if match:
 if locations[element] != None:
 print "WARNING: found %s in more than one place!" % element
 if not FORCE:
 answer = confirm("continue? (Y/n): ")
 if answer not in YES:
 fatal_error("USER ABORT")
 locations[element] = this_dir 
 for lib, loc in locations.items():
 if loc == None:
 print "WARNING: did not find library %s in directory tree of: %s" % (lib, directory)
 found_libraries = [lib for lib, loc in locations.items() if locations[lib] != None]
 if found_all_in_set(CORE_LIBS, found_libraries):
 platforms.append("core")
 if found_all_in_set(ANDROID_LIBS, found_libraries):
 platforms.append("android")
 if found_all_in_set(DESKTOP_LIBS, found_libraries):
 platforms.append("desktop")
 if found_all_in_set(GWT_LIBS, found_libraries):
 platforms.append("gwt")
 return platforms, locations
def found_all_in_set(lib_set, found_list):
 for lib in lib_set:
 if lib not in found_list:
 return False
 return True
def main(): 
 start_time = time.time()
 print "finding local libraries in %s" % PROJECT_DIR
 platforms, locations = search_for_lib_locations(PROJECT_DIR)
 if "core" not in platforms:
 fatal_error("did not find CORE libraries %s in project directory tree" % str(CORE_LIBS))
 else:
 print "found CORE libraries"
 for supported in SUPPORTED_PLATFORMS:
 if supported in platforms:
 print "found libraries for platform: %s" % supported.upper()
 else:
 print "WARNING: did not find libraries for platform: %s - WILL NOT UPDATE" % supported.upper()
 if ARCHIVE == None:
 print "checking latest nightly..."
 mtime = get_remote_archive_mtime()
 print "lastest nightly from server: %s" % mtime
 if not FORCE:
 answer = confirm("replace local libraries with files from latest nightly?(Y/n): ") 
 if answer not in YES:
 fatal_error("USER QUIT")
 libgdx = download_libgdx_zip() 
 else:
 if not os.path.exists(ARCHIVE):
 fatal_error("archive file not found: %s" % ARCHIVE)
 if not FORCE:
 answer = confirm("replace local libraries with files from '%s'?(Y/n): " % os.path.basename(ARCHIVE)) 
 if answer not in YES:
 fatal_error("USER QUIT")
 libgdx = open(ARCHIVE, "r")
 with zipfile.ZipFile(libgdx) as archive:
 if "core" in platforms: 
 run_core(locations, archive)
 if "desktop" in platforms:
 run_desktop(locations, archive)
 if "android" in platforms:
 run_android(locations, archive)
 if "gwt" in platforms:
 run_gwt(locations, archive)
 duration = time.time() - start_time 
 print "finished updates in %s" % human_time(duration)
 libgdx.close()
def title(text):
 dashes = "-" * 10
 print dashes + " %s " % text + dashes
if __name__ == "__main__":
 main()
Simon Forsberg
59.7k9 gold badges157 silver badges311 bronze badges
asked Jun 24, 2013 at 4:14
\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

Your code looks pretty good. Some notes:

  • According to PEP8, imports should be written in separate lines.

  • fatal_error: I'd probably write the signature this way: fatal_error(msg, code=1).

  • INTERACTIVE -> interactive. According to PEP8, global variables should be written lower-case.

  • That's an opinion: I prefer to write multi-line lists/dictionaries in JSON style. You save indentation space and the reordering of elements is straightforward (all at the meager cost of two lines):

    DESKTOP_LIBS = [
     "gdx-backend-lwjgl.jar",
     "gdx-backend-lwjgl-natives.jar",
     "gdx-natives.jar",
    ] 
    
  • Functions download_libgdx_zip and search_for_lib_locations are written (unnecessarily IMO) in a very imperative fashion. I'd probably refactor it with a functional approach in mind. At least don't reuse the same variable name to hold different values (i.e. total_size), as that takes away the sacred mathematical meaning of =.

  • Those functions run_xyz(locations, archive) look very similar, why not a unique run(platform, locations, archive).

  • Function found_all_in_set can be written:

    def found_all_in_set(lib_set, found_list):
     return all(lib in found_list for lib in lib_set)
    

    Or:

    def found_all_in_set(lib_set, found_list):
     return set(lib_list).issubset(set(found_list))
    
  • if ARCHIVE == None: -> if ARCHIVE is None: although I prefer the (almost) equivalent, more declarative if not ARCHIVE:.

  • There are a lot of imperative snippets that could be written functionally, for example this simple list-comprehension replaces a dozen lines from your code:

    platforms = [platform for (platform, libs) in zip(platforms, libs_list)
     if found_all_in_set(libs, found_libraries)]
    
answered Jun 24, 2013 at 8:38
\$\endgroup\$
4
  • \$\begingroup\$ From PEP8, the proper way to write if ARCHIVE == None: is if ARCHIVE is None. \$\endgroup\$ Commented Jun 24, 2013 at 9:15
  • \$\begingroup\$ @Josay: Sorry, I forgot a not, updated. I would write if ARCHIVE is None: only if I needed to specifically check that ARCHIVE is exactly None and not other falsy value ("" for example). It does not seem the case here. \$\endgroup\$ Commented Jun 24, 2013 at 9:24
  • \$\begingroup\$ Thanks for the great feedback! I can see Java making my Python very imperative as well. regarding "According to PEP8, global variables should be written lower-case." PEP8 also says constants should be all caps \$\endgroup\$ Commented Jun 24, 2013 at 15:00
  • \$\begingroup\$ @talloaktrees: A constant and a global variable are not the same. One is constant while the other is, well, variable :-) (either because it's different on each execution or because it's updated in-place). So YES is ok, but INTERACTIVE is not. \$\endgroup\$ Commented Jun 24, 2013 at 15:27

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.