#!/usr/bin/env python # # EDE installer from a single script # Copyright (C) 2005-2013 Sanel Zukan # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Note: patches (for patching part in this program) should be generated in form: # 'diff -uNr efltk/ efltksvn/ > efltk-2.0.6.patch' import sys import os import md5 import ConfigParser import tarfile __author__ = "Sanel Zukan " __version__ = "1.3" # from where to download config files netinstall_cfg_url = "http://equinox-project.org/netinstall-configs" # some mutable globals mirrors_url = [] patches_url = None prefix_path = "/usr/local" class NetinstallConfigParser(ConfigParser.ConfigParser): """ConfigParser with support for lists inside values, e.g: [section] key = [1, 2, 3, 4] """ def getlist(self, section, option): """Return a list of elements from given key. Note that splitting will be done by ',', no matter if string was quoted or not. """ value = self.get(section, option) if (value[0] == "[") and (value[-1] == "]"): elements = value[1:-1].split(",") elements = map(lambda x: x.strip().strip('"'), elements) return elements else: raise ConfigParser.ParsingError("Expected list as value in '%s' expression" % value) class Module: """The class that represents each item that will be downloaded, compiled and installed.""" def __init__(self): self.name = None self.package = None self.checksum = None self.patch = None self.url = None self.full_url = None # module will not be compiled if this executable was found self.skip_if_found = False # default self.build_cmds = ["./configure", "make", "make install"] def print_details(self): """Display module details.""" print("name : %s" % self.name) print("archive : %s" % self.package) print("checksum : %s" % self.checksum) print("patch : %s" % self.patch) print("build : %s" % self.build_cmds) def print_short_info(self): """Display module short info.""" print(" %s (%s)" % (self.name, self.package)) def do_message(msg, is_head = False): """Prints a message.""" if is_head: print("* %s" % msg) else: print(" %s" % msg) def do_error(err, quit = True): """Prints error message and quits.""" print("*** %s" % err) if quit: sys.exit(1) def file_remove(file): """Remove file if exists; if not, do nothing.""" try: os.unlink(file) except OSError: pass def file_exists(file): """Check if file exists and can be read.""" return os.access(file, os.F_OK | os.R_OK) def in_path(file): """Check if file is in PATH.""" lst = os.environ["PATH"].split(":") for l in lst: path = "%s/%s" % (l, file) if os.access(path, os.F_OK | os.X_OK): return True return False def fix_command_if_needed(cmd): """ Scan command and do some work: * if found ${PREFIX}, replace it with user/default prefix content * if found 'export', call python functions for exporting, since os.system can't do that """ def replace_variables(cmd): return cmd.replace("${PREFIX}", prefix_path) if cmd.startswith("export "): kv = cmd.split()[1] k, v = kv.split("=") os.environ[k] = replace_variables(v) return None return replace_variables(cmd) def download_from(url, save_as, show_progress = True): """Download from the given url.""" # didn't put inport at the top since it's slows startup time import urllib kbs = 1024 mbs = kbs * 1024 if show_progress: # callback for urllib.urlretrieve(); shows some percentage def download_report_hook(transfered, block_size, total): # in case we are not able to get package size from server, just print out # currently received size if total < 0: sz = transfered * block_size sys.stdout.write("\r [%i B]" % sz) sys.stdout.flush() return if total > mbs: # use float so we can get better precision total_str = "%.1f MB" % (float(total) / mbs) elif total > kbs: total_str = "%.1f kB" % (float(total) / kbs) else: total_str = "%i B" % total percent = (transfered * block_size * 100) / total sys.stdout.write("\r [%i%% of %s]" % (percent, total_str)) sys.stdout.flush() try: if show_progress: urllib.urlretrieve(url, save_as, download_report_hook) sys.stdout.write("\n") else: urllib.urlretrieve(url, save_as) except IOError, e: do_error("Unable to download '%s'. Got: %s" % (save_as, e)) def fetch_and_parse_config(config_name, is_local): """ Download config file and parse it. If 'is_local' is true, 'config_name' will be seen as local file. """ global mirrors_url global patches_url # default is to download it if not is_local: path = "%s/%s" % (netinstall_cfg_url, config_name) download_from(path, config_name, False) #config = ConfigParser.ConfigParser() config = NetinstallConfigParser() try: config.read(config_name) except ConfigParser.MissingSectionHeaderError, err: file_remove(config_name) do_error("Fatal: Unable to parse config file.\nOutput: %s" % err, False) do_error("Probably the wrong file was downloaded or the target config file wasn't found.", False) do_error("Please try again later, and if the problem persists, report this as bug.") # check first if we need a newer script if config.has_option("General", "requires"): req = config.getfloat("General", "requires") if req > float(__version__): do_error("", False) do_error("You have an older program version, but this configuration file requires something newer.", False) do_error("You can get the latest version at 'http://equinox-project.org/netinstall'", False) do_error("") # optional, but if any of modules has 'patch' entry, script will report how this is missin if config.has_option("General", "patches"): patches_url = config.get("General", "patches") # something we must have if not config.has_option("General", "mirrors"): do_error("Unable to find 'mirrors' section") if not config.has_option("General", "modules"): do_error("Unable to find 'modules' section") mirrors_url = config.get("General", "mirrors").strip('"').split("\n") modules = config.get("General", "modules").strip('"').split(",") # remove spaces mirrors_url = [i.strip() for i in mirrors_url] modules = [i.strip() for i in modules] modules_objects = [] # now parse each secion for i in modules: m = Module() m.name = i # mandatory keys if not config.has_option(i, "package"): do_error("Module '%s' is missing a 'package' section" % i) else: m.package = config.get(i, "package").strip('"') if config.has_option(i, "checksum"): m.checksum = config.get(i, "checksum").strip('"') # optional keys if config.has_option(i, "build"): m.build_cmds = config.getlist(i, "build") if config.has_option(i, "skip_if_found"): m.skip_if_found = config.get(i, "skip_if_found").strip('"') if config.has_option(i, "url"): m.url = config.get(i, "url").strip('"') if config.has_option(i, "full_url"): m.full_url = config.get(i, "full_url") # this depends on global 'patches' key; if not existing, from where to fetch the patches then ;) if config.has_option(i, "patch"): if not patches_url: do_error("Module '%s' needs to be patched, but global patches url is not given" % i) m.patch = config.get(i, "patch").strip('"') # add it to the list if possible if m.skip_if_found and in_path(m.skip_if_found): do_message("Skipping '%s' since the command '%s' is found" % (m.name, m.skip_if_found), True) continue else: modules_objects.append(m) return modules_objects def validate_md5sum(module): """Compute md5 checksum and compare it to the existing one.""" if not file_exists(module.package): return False # in case we are not interested in checksum if module.checksum == None: return True m = md5.new() f = open(module.package).read() m.update(f) return (m.hexdigest() == module.checksum) def decompress_file(file): """Decompress file using python library. Returns a directory name that contains the source tree.""" should_extract = True dirname = None tar_mtime = None stamp_filename = ".netinstall_extract_stamp" try: # open it transparently, since python 2.4 (and older), does not understainds 'r:*' expression tarobj = tarfile.open(file, "r") except tarfile.ReadError, strerror: do_error("Unable to open archive: '%s'" % strerror) except IOError: do_error("Unable to open '%s' file" % file) # recort ctime tar_mtime = os.stat(file).st_mtime tar_mtime = int(tar_mtime) # check if we need to extract it at all by comparing archive stamp against # 'dirname/stamp_filename' if that file exists first = tarobj.next() if first.isdir(): # tarfile keeps two slashes at the end dirname = first.name.strip("/") stamp_file = "%s/%s" % (dirname, stamp_filename) if file_exists(stamp_file): s = open(stamp_file).readline().strip("\n") # in case we read some garbage or something, just see it as should be extracted try: s = int(s) except ValueError: # so below 'if' can fail s = -tar_mtime # real check if tar_mtime <= s: should_extract = False if not should_extract: return dirname # extract files; tarfile.extractall() is not used since it is available from python 2.5 for f in tarobj: if f.isdir(): try: os.makedirs(f.name) except OSError, e: if e.errno == 17: pass else: do_error("Unable to make directory '%s' (%s : %i)" % (f.name, e.strerror, e.errno)) else: tarobj.extract(f) # if we got directory, write stamp checker: if dirname: stamp_file = "%s/%s" % (dirname, stamp_filename) try: f = open(stamp_file, "w") f.write("%i\n" % tar_mtime) f.close() except IOError: pass return dirname def fetch_module(module): """Handles downloading assuring download were succedded. Also downloads it's patches if they exists""" # file is already downloaded if validate_md5sum(module): return got_file = False do_message("Downloading %s..." % module.package, True) # use 'url' if module provides it, skipping global mirrors_url if module.url: urls = [module.url] else: urls = mirrors_url for mirror in urls: # when was set 'full_url = true', this should indicate how module 'url' section contains # full path to file name (url + filename) and it will be saved as 'module.package' # # this useful for retrieving files where source file is generated (e.g. viewvc) and should be stored # as specific name if module.full_url == "true": path = module.url else: path = "%s/%s" % (mirror, module.package) download_from(path, module.package) # check if the file is correct if validate_md5sum(module): got_file = True break else: # remove what we downloaded (e.g. if sourceforge mirror does not has the file, it will # return html page and urllib will download it) file_remove(module.package) if not got_file: do_error("Unable to download '%s' package. Please try again" % module.package) # now, check for patches if module.patch and patches_url: path = "%s/%s" % (patches_url, module.patch) do_message("Getting the patch for %s..." % module.package, True) download_from(path, module.patch) def extract_compile_and_install_module(module): """Extract, patch, compile and install given package.""" do_message("Extracting %s..." % module.package, True) dirname = decompress_file(module.package) # assure we have something assert(dirname != None) # do actual patching if module.patch: if not in_path("patch"): do_error("Fatal: this module needs to be patched but 'patch' tool can't be found. Please install 'patch' and run this script again.") if not file_exists(module.patch): do_error("Fatal: module patch was downloaded, but can't be found. Please try again.") do_message("Patching %s..." % module.package, True) os.system("patch -t -p0 < %s" % module.patch) # top directory, so we know how to return top_dir = os.getcwd() # go in this directory os.chdir(dirname) do_message("Building %s..." % module.name, True) # execute commands in the order for cmd in module.build_cmds: cmd = fix_command_if_needed(cmd) # continue, as 'fix_command_if_needed' already done some work if(cmd == None): continue ret = os.system(cmd) if ret > 0: do_error("'%s' failed, receiving '%i' from the shell. Please check the output and see if something can be fixed" % (cmd, ret)) os.chdir(top_dir) def check_prerequisites(check_for_root): """Mandatory things before we do anything useful.""" if check_for_root and not os.getuid() == 0: do_error("You must be a root to run this tool. Since some packages depends on each other", False) do_error("they must be system wide installed. On other hand, if you are not sure what this script", False) do_error("can do, you can always check it's source code or directly ask developers.", False) do_error("", False) do_error("If you would like to run this script anyway, use '--no-root-check' flag, but this", False) do_error("will probably result with failed build.", False) do_error("", False) do_error("NOTE: always download this script ONLY from http://equinox-project.org") def list_config_files(): """Downloads file with config list and display it to the user.""" target = "netinstall-config-list.txt" path = "%s/%s" % (netinstall_cfg_url, target) download_from(path, target, False) try: content = open(target).read() except IOError, e: do_error("Fatal: Unable to open fetched file (%s)" % e.strerror) # remove the file file_remove(target) # check if we got our file by simple checking the '## Config list ##' header; this header # must be in the first line if content[:17] != "## Config list ##": do_error("Managed to fetch a list, but seems how the list contains a garbage. Please try again") print(content) def help(): print("Usage: netinstall [OPTIONS]") print("Download, compile and install EDE with a single command") print("") print("Options:") print(" --help this help") print(" --version show script version") print("") print(" --prefix [PATH] set where all modules will be installed (e.g. /usr or /opt; default is /usr/local)") print("") print(" --module-info [NAME] display information about [NAME] module") print(" --show-modules list modules that will be compiled") print(" --build-dir [NAME] directory where modules will be downloaded and compiled (default is 'build')") print(" --config [NAME] use [NAME] as main config file (default is 'netinstall.cfg')") print(" --config-local [NAME] use local config (only for debugging)") print(" --no-root-check do not check if script was started as root (only for debugging)") print(" --list-configs lists known config files with their description") print("") print("Report bugs at ") def main(): show_modules = False module_name = None build_dir = "build" config_name = "netinstall.cfg" config_local = None check_for_root = True if len(sys.argv) > 1: try: import getopt opts, args = getopt.getopt(sys.argv[1:], "h", ["help", "version", "prefix=", "show-modules", "module-info=", "build-dir=", "config=", "config-local=", "no-root-check", "list-configs"]) except getopt.GetoptError: print("Bad option; rerun 'netinstall --help' for command details") return for o, a in opts: if o == "--help" or o == "-h": help() return if o == "--version": print(__version__) return if o == "--show-modules": show_modules = True elif o == "--module-info": module_name = a elif o == "--build-dir": build_dir = a elif o == "--config": config_name = a elif o == "--config-local": config_local = a elif o == "--no-root-check": check_for_root = False elif o == "--list-configs": list_config_files() return elif o == "--prefix": global prefix_path prefix_path = a # do prerequisites first check_prerequisites(check_for_root) # get the config first and read it if config_local: config_name = config_local # in case suffix wasn't added, add it if not config_name.endswith(".cfg"): config_name += ".cfg" modules = fetch_and_parse_config(config_name, config_local != None) # show a list of modules if show_modules: for i in modules: i.print_short_info() return # show module details if module_name: found = False for i in modules: if i.name == module_name: i.print_details() found = True break if not found: do_error("Module '%s' wasn't found" % module_name) return # for building part, create build directory and go in it try: os.makedirs(build_dir) except OSError, e: # 17 means directory already exists and that is ok if e.errno != 17: do_error("Unable to create directory %s (%s)" % (build_dir, e.strerror)) # a directory where we started; full path is needed so we can return to it at the end starting_dir = os.getcwd() # go into build directory os.chdir(build_dir) # or, do the main job; first download all packages for i in modules: fetch_module(i) # by now we are sure all files are downloaded correctly so we can # proceed to compile and install each of them for i in modules: extract_compile_and_install_module(i) # return where we started os.chdir(starting_dir) if __name__ == "__main__": main()