#!/usr/bin/env python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2014             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk 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 in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# tails. You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

###################################################
# IMPORTANT: THIS FILE EXISTS TWICE
# ANY CHANCES HERE ARE ALSO REQUIRED IN THE COUNTERPART
# check_mk.git ~/share/check_mk/agents/windows/msibuild
# cmk-omd.git  ~/packages/msitools
####################################################

import os, sys, uuid, tempfile, shutil, re

def verbose(text):
    if opt_verbose:
        sys.stdout.write(text + "\n")

def bail_out(text):
    sys.stderr.write("ERROR: %s\n" % text)
    sys.exit(1)

try:
    if sys.argv[1] == '-v':
        opt_verbose = True
        del sys.argv[1]
    else:
        opt_verbose = False

    # MSI container to modify
    msi_file          = sys.argv[1]

    # Directory where the sources are contained
    source_dir        = sys.argv[2]

    # Revision (from build_version)
    revision  = sys.argv[3]

    # TODO: complete overhaul of version generation
    # Official version name, e.g
    # 1.2.5i4p1
    # 2015.04.12
    # 1.2.6-2015.04.12
    new_version_name = sys.argv[4]
    major, minor, build = 1, 0, 0
    try:
        major, minor, build = map(lambda x: x.lstrip("0"),
                                  new_version_name.split("-")[0].split(".")[:3])
        build = build == "" and "0" or build
        if len(major) > 3:
            # Looks like a daily build.. 2015.03.05
            major = major[2:].lstrip("0")
    except:
        pass
    new_product_version = "%s.%s.%s" % (major, minor, build)

    # Remove any traces of i, p, b versions. Windows can't handle them...
    # The revision should be enough to uniquely identify this build
    # The original version name is also still visible in the list of programs
    match = re.search("[a-z]", new_product_version)
    if match:
        new_version_build = new_product_version[:match.start(0)]
        if new_version_build[-1] == ".":
            new_version_build += "0"
        new_version_build += ".%s" % revision
    else:
        new_version_build = "%s.%s" % (new_product_version, revision)

except:
    bail_out("Usage: %s MSIFILE.msi SOURCEDIR BUILDNUMBER VERSION" % sys.argv[0])


try:
    if "OMD_ROOT" in os.environ:
        path_prefix = ""
        tmp_dir = os.environ["OMD_ROOT"] + "/tmp"
    else:
        path_prefix = "./"
        tmp_dir = "."

    new_msi_file = "check_mk_agent.msi"
    work_dir = tempfile.mkdtemp(prefix = tmp_dir + "/msi-update.")

    # When this script is run in the build environment then we need to specify
    # paths to the msitools. When running in an OMD site, these tools are in
    # our path

    # Export required idt files into work dir
    for entry in [ "File", "Property", "Component" ]:
        verbose("Export table %s from file %s" % (entry, msi_file))
        if os.system((path_prefix + "msiinfo export %(msi_file)s %(property)s > %(work_dir)s/%(property)s.idt") % \
                 { "work_dir" : work_dir, "msi_file": msi_file, "property": entry }) != 0:
            bail_out('Failed to unpack msi table %s from %s' % (entry, msi_file))

    verbose("Modify extracted files..")

    # ==============================================
    # Modify File.idt

    # HACK: the 64 bit agent is msi internally handled as check_mk_agent64.exe
    os.rename(source_dir + "/check_mk_agent-64.exe", source_dir + "/check_mk_agent64.exe")


    lines_file_idt = file(work_dir + "/File.idt").readlines()
    file_idt_new   = file(work_dir + "/File.idt.new", "w")
    file_idt_new.write("".join(lines_file_idt[:3]))

    cabinet_files = []
    for line in lines_file_idt[3:]:
        tokens = line.split("\t")
        filename = tokens[0]
        cabinet_files.append((tokens[-1], filename))
        file_stats = os.stat(source_dir + "/" + filename)
        new_size    = file_stats.st_size
        tokens[3] = str(new_size)
        # The version of this file is different from the msi installer version !
        tokens[4] = tokens[4] and new_version_build or ""
        tokens[4] = tokens[4] and new_version_build or ""
        file_idt_new.write("\t".join(tokens))
    file_idt_new.close()
    # ==============================================


    # ==============================================
    # Modify Component.idt
    lines_component_idt = file(work_dir + "/Component.idt").readlines()
    component_idt_new   = file(work_dir + "/Component.idt.new", "w")
    component_idt_new.write("".join(lines_component_idt[:3]))

    for line in lines_component_idt[3:]:
        tokens = line.split("\t")
        if tokens[0] in [ "plugins.cap",
                          "check_mk.ini",
                          "agent32",
                          "agent64",
                          "check_mk.example.ini" ]:
            tokens[1] = ("{%s}" % uuid.uuid1()).upper()
        component_idt_new.write("\t".join(tokens))

    component_idt_new.close()

    # ==============================================

    # ==============================================
    # Modify Property.idt
    product_code = ("{%s}\r\n" % uuid.uuid1()).upper()
    lines_property_idt = file(work_dir + "/Property.idt").readlines()
    property_idt_new   = file(work_dir + "/Property.idt.new", "w")
    property_idt_new.write("".join(lines_property_idt[:3]))

    for line in lines_property_idt[3:]:
        tokens = line.split("\t")
        if tokens[0] == "ProductName":
            tokens[1] = "Check_MK Agent\r\n"
    # The upgrade code defines the product family. Do not change it!
    #    elif tokens[0] == "UpgradeCode":
    #        tokens[1] = upgrade_code
        elif tokens[0] == "ProductCode":
            tokens[1] = product_code
        elif tokens[0] == "ProductVersion":
            tokens[1] = "%s\r\n" % ".".join(new_version_build.split(".")[:4])
        property_idt_new.write("\t".join(tokens))
    property_idt_new.close()
    # ==============================================


    verbose("Creating copy of original file %s -> %s" % (msi_file, new_msi_file))

    # Make a copy
    if os.system("cp %(msi_file)s %(new_msi_file)s" % { "msi_file": msi_file, "new_msi_file": new_msi_file}) != 0:
        bail_out('Fehler!')

    # Rename modified tables
    for entry in [ "Property", "File", "Component" ]:
        os.rename(work_dir + "/%s.idt.new" % entry , work_dir + "/%s.idt" % entry)

    for entry in [ "Property", "File", "Component" ]:
        if os.system((path_prefix + "msibuild %(new_msi_file)s -i %(work_dir)s/%(file)s.idt") % \
             { "work_dir" : work_dir, "new_msi_file": new_msi_file, "file": entry }) != 0:
            bail_out('Fehler!')

    # Update summary info with new uuid (HACK! - the msibuild tool is not able to do this on all systems)
    # In this step we replace the package code with a new uuid. This uuid is important, because it is
    # the unqiue identifier for this package. Inside the package the uuid is split into two halfs.
    # Each of it is updated with the corresponding new package code. Since msi-update uses the
    # vanilla_container.msi as well as the baked_container.msi we need to handle both..
    package_code = ("{%s}" % uuid.uuid1()).upper()
    summary_info = {"new_msi_file": new_msi_file, "start": package_code[:16], "end": package_code[16:]}
    default_package_codes = {
            "vanilla_container.msi": { "dftl_start": "{08012468-53DE-4", "dftl_end": "2A3-BAA9-FE4C2A129135}"},
            "baked_container.msi"  : { "dftl_start": "{47ED1918-CEDF-4", "dftl_end": "D37-943C-B4DBC3F58E6C}"}
    }
    summary_info.update(default_package_codes[os.path.basename(msi_file)])
    if os.system("sed -i -e 's/%(dftl_start)s/%(start)s/' "
                 "-e 's/%(dftl_end)s/%(end)s/' %(new_msi_file)s" % summary_info) != 0:
        bail_out('Fehler!')

    # Remove original product.cab from stream
    verbose("Removing product.cab from %s" % new_msi_file)
    if os.system((path_prefix + "msibuild %(new_msi_file)s -q \"DELETE FROM _Streams "
                 "where Name = 'product.cab'\"") % { "new_msi_file": new_msi_file } ) != 0:
        bail_out('Fehler!')

    # Prepare product.cab file
    verbose("Generating new product.cab")

    lcab_files = ""
    for index, lcab_file in sorted(cabinet_files):
        lcab_files += "%(source_dir)s/%(lcab_file)s " % globals()
    if os.system("lcab -n %(lcab_files)s %(work_dir)s/product.cab" % globals()) != 0:
        bail_out('Fehler!')

    # Add modified product.cab
    verbose("Add modified product.cab")
    if os.system((path_prefix + "msibuild %(new_msi_file)s -a product.cab %(work_dir)s/product.cab") % \
            { "work_dir" : work_dir, "new_msi_file": new_msi_file }) != 0:
        bail_out('Fehler!')

    shutil.rmtree(work_dir)
    verbose("Successfully created file " + new_msi_file)
except Exception, e:
    if work_dir and os.path.exists(work_dir):
        shutil.rmtree(work_dir)
    bail_out("Error on creating msi file: %s" % str(e))

