#!/usr/bin/env python3
# mass-build - Easily Build Software Involving a Large Amount of Source Repositories
# Copyright (C) 2012-2013 Ralf Jung <post@ralfj.de>
#
# 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.

import vcs, build_system
import argparse, os, sys, subprocess
from collections import OrderedDict

# helper functions
def load_module(name, path, write_bytecode = False):
    old_val = sys.dont_write_bytecode
    sys.dont_write_bytecode = not write_bytecode
    module = None
    try:
        from importlib.machinery import SourceFileLoader
        module = SourceFileLoader(name, path).load_module()
    except ImportError:
        import imp
        module = imp.load_source(name, path)
    finally:
        sys.dont_write_bytecode = old_val
    return module

# an entire Project
class Project:
    def __init__(self, folder, config):
        self.folder = folder
        self.name = config['name']
        # VCS
        vcsName = config['vcs']
        if vcsName == 'git':
            self.vcs = vcs.Git(self.sourceFolder(), config)
        elif vcsName == 'svn':
            self.vcs = vcs.SVN(self.sourceFolder(), config['url'])
        else:
            raise Exception("Unknown VCS type "+vcsName)
        # build system
        if config.get('buildDeb', False):
            self.buildSystem = build_system.AutoDebuild(self.sourceFolder(), self.buildFolder(), config, self.vcs)
        else:
            buildSystemName = config['buildSystem']
            if buildSystemName == 'cmake':
                self.buildSystem = build_system.CMake(self.sourceFolder(), self.buildFolder(), config)
            else:
                raise Exception("Unknown build system type "+buildSystemName)
    
    def sourceFolder(self):
        return os.path.join(self.folder, self.name)
    
    def buildFolder(self):
        return os.path.join(config['buildDir'], self.sourceFolder())

# read command-line arguments
parser = argparse.ArgumentParser(description='Update and build a bunch of stuff')
parser.add_argument("-c", "--config",
                    dest="config", default="mass-build.conf",
                    help="mass-build config file")
parser.add_argument("--reconfigure",
                    action="store_true", dest="reconfigure",
                    help="Force configuration to be run")
parser.add_argument("-w", "--wait-after-config",
                    action="store_true", dest="wait_after_config",
                    help="Wait for user confirmation after configuration is finished")
parser.add_argument("--reset-source",
                    action="store_true", dest="reset_source",
                    help="Reset sourcecode to the given version (removes local changes!)")
parser.add_argument("--no-update",
                    action="store_false", dest="update",
                    help="Do not update projects before compilation")
parser.add_argument("--resume-from", metavar='PROJECT',
                    dest="resume_from",
                    help="From the projects specified, continue building with this one (i.e., remove all projects before this one from the list - this never adds new projects)")
parser.add_argument("--check-versions",
                    action="store_true", dest="version_check",
                    help="Check the repositories for newer tags, if possible (does not perform any building steps)")
parser.add_argument("projects",  metavar='PROJECT', nargs='*',
                    help="Manually specify projects or folders to be built (project names take precedence)")
args = parser.parse_args()
if args.reset_source and not args.update:
    raise Exception("Can not reset sources without doing an update")

# load config as dictionary
config = vars(load_module('config', args.config))

# initialise variables holding the configuration
allProjects = OrderedDict() # all projects
allFolders = {} # all folders
workProjects = [] # projects we work on

# copy all items which don't exist below, except for those in the exclude list
def inherit(subConfig, superConfig, exclude = ('name', 'projects')):
    for name in superConfig.keys():
        if (not name in subConfig) and (not name in exclude):
            subConfig[name] = superConfig[name]

# populate list of projects, return list of projects in that folder
def loadProjects(config, folder=''):
    folderProjects = []
    for projectConfig in config['projects']:
        assert 'name' in projectConfig # everything must have a name
        inherit(projectConfig, config)
        if 'projects' in projectConfig: # a subpath
            folderProjects += loadProjects(projectConfig, os.path.join(folder, projectConfig['name']))
        else: # a proper project
            if projectConfig['name'] in allProjects:
                raise Exception("Duplicate project name "+projectConfig['name'])
            project = Project(folder, projectConfig)
            allProjects[projectConfig['name']] = project
            folderProjects.append(project)
    # store projects of this folder
    if folder in allFolders:
        raise Exception("Duplicate folder name "+folder)
    allFolders[folder] = folderProjects
    return folderProjects

# load available projects
loadProjects(config)
# get base set og projects to process
if args.projects:
    for name in args.projects:
        if name in allProjects:
            workProjects.append(allProjects[name])
        elif name in allFolders:
            workProjects += allFolders[name]
        else:
            raise Exception("Project or folder %s does not exist" % name)
else:
    workProjects = list(allProjects.values()) # all the projects
# apply the "resume from"
if args.resume_from is not None:
    # find project index
    startIndex = 0
    while startIndex < len(workProjects):
        if workProjects[startIndex].name == args.resume_from:
            break # we found it
        else:
            startIndex += 1
    if startIndex >= len(workProjects): # project not found
        raise Exception("%s not found in list of projects to work on" % args.resume_from)
    # start here
    workProjects = workProjects[startIndex:]

# and do it!
for project in workProjects:
    try:
        if args.version_check:
            print("Checking project",project.sourceFolder())
            project.vcs.checkVersions()
        else:
            if args.update:
                print("Updating project",project.sourceFolder())
                project.vcs.update(mode = vcs.MODE_RESET if args.reset_source else vcs.MODE_REBASE)
            print("Building project",project.sourceFolder())
            project.buildSystem.build(reconfigure=args.reconfigure, waitAfterConfig=args.wait_after_config)
        print()
    except (subprocess.CalledProcessError, KeyboardInterrupt) as e: # for some exceptions, a stackrace is usually pointless
        print(file=sys.stderr)
        print(file=sys.stderr)
        if isinstance(e, KeyboardInterrupt): # str(e) would be the empty string
            print("Interruped by user while processing %s" % (project.name), file=sys.stderr)
        else:
            print("Error while processing %s: %s" % (project.name, str(e)), file=sys.stderr)
        print(file=sys.stderr)
        sys.exit(1)
print("All operations successfully completed")
