#!/usr/bin/python
# ----------------------------------------------------------------------------
# cocos2d "compile" plugin
#
# Copyright 2013 (C) Luis Parravicini
#
# License: MIT
# ----------------------------------------------------------------------------
'''
"compile" plugin for cocos command line tool
'''

__docformat__ = 'restructuredtext'

import multiprocessing
import cocos
from MultiLanguage import MultiLanguage
import cocos_project
import os
import re
import sys
import shutil
import json
from . import build_web
import utils

class CCPluginCompile(cocos.CCPlugin):
    """
    compiles a project
    """

    BUILD_CONFIG_FILE = "build-cfg.json"
    CFG_KEY_WIN32_COPY_FILES = "copy_files"
    CFG_KEY_WIN32_MUST_COPY_FILES = "must_copy_files"

    CFG_KEY_COPY_RESOURCES = "copy_resources"
    CFG_KEY_MUST_COPY_RESOURCES = "must_copy_resources"

    OUTPUT_DIR_NATIVE = "bin"
    OUTPUT_DIR_SCRIPT_DEBUG = "simulator"
    OUTPUT_DIR_SCRIPT_RELEASE = "publish"
    WEB_PLATFORM_FOLDER_NAME = "html5"

    PROJ_CFG_KEY_IOS_SIGN_ID = "ios_sign_id"
    PROJ_CFG_KEY_ENGINE_DIR = "engine_dir"

    BACKUP_SUFFIX = "-backup"
    ENGINE_JS_DIRS = [
        "frameworks/js-bindings/bindings/script",
        "cocos/scripting/js-bindings/script"
    ]

    CMAKE_VS_GENERATOR_MAP = {
        "12" : "Visual Studio 12 2013",
        "14" : "Visual Studio 14 2015",
        "15" : "Visual Studio 15 2017",
        "16" : "Visual Studio 16 2019",
    }

    @staticmethod
    def plugin_name():
        return "compile"

    @staticmethod
    def brief_description():
        return MultiLanguage.get_string('COMPILE_BRIEF')

    def _add_custom_options(self, parser):
        from argparse import ArgumentParser
        parser.add_argument("-m", "--mode", dest="mode", default='debug',
                          help=MultiLanguage.get_string('COMPILE_ARG_MODE'))
        parser.add_argument("-j", "--jobs", dest="jobs", type=int,
                          help=MultiLanguage.get_string('COMPILE_ARG_JOBS'))
        parser.add_argument("-o", "--output-dir", dest="output_dir",
                            help=MultiLanguage.get_string('COMPILE_ARG_OUTPUT'))

        group = parser.add_argument_group(MultiLanguage.get_string('COMPILE_ARG_GROUP_ANDROID'))
        group.add_argument("--ap", dest="android_platform",
                           help=MultiLanguage.get_string('COMPILE_ARG_AP'))
        group.add_argument("--build-type", dest="build_type",
                           help=MultiLanguage.get_string('COMPILE_ARG_BUILD_TYPE'))
        group.add_argument("--app-abi", dest="app_abi",
                           help=MultiLanguage.get_string('COMPILE_ARG_APP_ABI'))
        group.add_argument("--ndk-toolchain", dest="toolchain",
                           help=MultiLanguage.get_string('COMPILE_ARG_TOOLCHAIN'))
        group.add_argument("--ndk-cppflags", dest="cppflags",
                           help=MultiLanguage.get_string('COMPILE_ARG_CPPFLAGS'))
        group.add_argument("--no-apk", dest="no_apk", action="store_true",
                           help=MultiLanguage.get_string('COMPILE_ARG_NO_APK'))
        group.add_argument("--no-sign", dest="no_sign", action="store_true",
                           help=MultiLanguage.get_string('COMPILE_ARG_NO_SIGN'))

        group = parser.add_argument_group(MultiLanguage.get_string('COMPILE_ARG_GROUP_WIN'))
        group.add_argument("--vs", dest="vs_version", type=int,
                           help=MultiLanguage.get_string('COMPILE_ARG_VS'))

        group = parser.add_argument_group(MultiLanguage.get_string('COMPILE_ARG_GROUP_WEB'))
        group.add_argument("--source-map", dest="source_map", action="store_true",
                           help=MultiLanguage.get_string('COMPILE_ARG_SOURCE_MAP'))
        group.add_argument("--advanced", dest="advanced", action="store_true",
                           help=MultiLanguage.get_string('COMPILE_ARG_ADVANCE'))

        group = parser.add_argument_group(MultiLanguage.get_string('COMPILE_ARG_GROUP_IOS_MAC'))
        group.add_argument("-t", "--target", dest="target_name",
                           help=MultiLanguage.get_string('COMPILE_ARG_TARGET'))

        group = parser.add_argument_group(MultiLanguage.get_string('COMPILE_ARG_GROUP_IOS'))
        group.add_argument("--sign-identity", dest="sign_id",
                           help=MultiLanguage.get_string('COMPILE_ARG_IOS_SIGN'))
        group.add_argument("-sdk", dest="use_sdk", metavar="USE_SDK", nargs='?', default='iphonesimulator',
                           help=MultiLanguage.get_string('COMPILE_ARG_IOS_SDK'))

        group = parser.add_argument_group(MultiLanguage.get_string('COMPILE_ARG_GROUP_LUA_JS'))
        group.add_argument("--no-res", dest="no_res", action="store_true",
                           help=MultiLanguage.get_string('COMPILE_ARG_NO_RES'))
        group.add_argument("--compile-script", dest="compile_script", type=int, choices=[0, 1],
                           help=MultiLanguage.get_string('COMPILE_ARG_COMPILE_SCRIPT'))

        group = parser.add_argument_group(MultiLanguage.get_string('COMPILE_ARG_GROUP_LUA'))
        group.add_argument("--lua-encrypt", dest="lua_encrypt", action="store_true",
                           help=MultiLanguage.get_string('COMPILE_ARG_LUA_ENCRYPT'))
        group.add_argument("--lua-encrypt-key", dest="lua_encrypt_key",
                           help=MultiLanguage.get_string('COMPILE_ARG_LUA_ENCRYPT_KEY'))
        group.add_argument("--lua-encrypt-sign", dest="lua_encrypt_sign",
                           help=MultiLanguage.get_string('COMPILE_ARG_LUA_ENCRYPT_SIGN'))

        category = self.plugin_category()
        name = self.plugin_name()
        usage = "\n\t%%prog %s %s -p <platform> [-s src_dir][-m <debug|release>]" \
                "\nSample:" \
                "\n\t%%prog %s %s -p android" % (category, name, category, name)

    def _check_custom_options(self, args):
        # get the mode parameter
        available_modes = [ 'release', 'debug' ]
        self._mode = self.check_param(args.mode, 'debug', available_modes,
                                      MultiLanguage.get_string('COMPILE_ERROR_WRONG_MODE_FMT',
                                                               available_modes))

        # android arguments
        available_build_types = [ 'cmake', 'none']
        self._build_type = self.check_param(args.build_type, 'cmake', available_build_types,
                                          MultiLanguage.get_string('COMPILE_ERROR_WRONG_BUILD_TYPE_FMT',
                                                                   available_build_types))
        self._no_apk = args.no_apk
        self._no_sign = args.no_sign

        self.app_abi = None
        if args.app_abi:
            self.app_abi = " ".join(args.app_abi.split(":"))

        self.cppflags = None
        if args.cppflags:
            self.cppflags = args.cppflags

        self.ndk_toolchain = None
        if args.toolchain:
            self.ndk_toolchain = args.toolchain

        # Win32 arguments
        self.vs_version = args.vs_version

        # iOS/Mac arguments
        self.xcode_target_name = None
        if args.target_name is not None:
            self.xcode_target_name = args.target_name

        if args.compile_script is not None:
            self._compile_script = bool(args.compile_script)
        else:
            self._compile_script = (self._mode == "release")

        self._ap = args.android_platform

        if args.jobs is not None:
            self._jobs = args.jobs
        else:
            self._jobs = self.get_num_of_cpu()
        self._has_sourcemap = args.source_map
        self._web_advanced = args.advanced
        self._no_res = args.no_res

        if args.output_dir is None:
            self._output_dir = self._get_output_dir()
        else:
            if os.path.isabs(args.output_dir):
                self._output_dir = args.output_dir
            else:
                self._output_dir = os.path.abspath(args.output_dir)

        self._sign_id = args.sign_id
        self._use_sdk = args.use_sdk

        if self._project._is_lua_project():
            self._lua_encrypt = args.lua_encrypt
            self._lua_encrypt_key = args.lua_encrypt_key
            self._lua_encrypt_sign = args.lua_encrypt_sign

        self.end_warning = ""
        self._gen_custom_step_args()

    def check_param(self, value, default_value, available_values, error_msg, ignore_case=True):
        if value is None:
            return default_value

        if ignore_case:
            check_value = value.lower()
            right_values = []
            for v in available_values:
                right_values.append(v.lower())
        else:
            check_value = value
            right_values = available_values

        if check_value in right_values:
            return check_value
        else:
            raise cocos.CCPluginError(error_msg, cocos.CCPluginError.ERROR_WRONG_ARGS)

    def get_num_of_cpu(self):
        try:
            return multiprocessing.cpu_count()
        except Exception:
            print(MultiLanguage.get_string('COMPILE_DETECT_CPU_FAILED'))
            return 1

    def _get_output_dir(self):
        project_dir = self._project.get_project_dir()
        cur_platform = self._platforms.get_current_platform()
        if self._project._is_script_project():
            if self._project._is_js_project() and self._platforms.is_web_active():
                cur_platform = CCPluginCompile.WEB_PLATFORM_FOLDER_NAME

            if self._mode == 'debug':
                output_dir = os.path.join(project_dir, CCPluginCompile.OUTPUT_DIR_SCRIPT_DEBUG, cur_platform)
            else:
                output_dir = os.path.join(project_dir, CCPluginCompile.OUTPUT_DIR_SCRIPT_RELEASE, cur_platform)
        else:
            output_dir = os.path.join(project_dir, CCPluginCompile.OUTPUT_DIR_NATIVE, self._mode, cur_platform)

        return output_dir

    def _gen_custom_step_args(self):
        self._custom_step_args = {
            "project-path": self._project.get_project_dir(),
            "platform-project-path": self._platforms.project_path(),
            "build-mode": self._mode,
            "output-dir": self._output_dir
        }

        if self._platforms.is_android_active():
            self._custom_step_args["ndk-build-type"] = self._build_type

    def _build_cfg_path(self):
        cur_cfg = self._platforms.get_current_config()
        if self._platforms.is_win32_active():
            ret = self._platforms.project_path()
        elif self._platforms.is_ios_active():
            ret = os.path.join(self._platforms.project_path(), "ios")
        elif self._platforms.is_mac_active():
            ret = os.path.join(self._platforms.project_path(), "mac")
        else:
            ret = self._platforms.project_path()

        return ret

    def _update_build_cfg(self):
        build_cfg_dir = self._build_cfg_path()
        cfg_file_path = os.path.join(build_cfg_dir, CCPluginCompile.BUILD_CONFIG_FILE)
        if not os.path.isfile(cfg_file_path):
            return

        key_of_copy = None
        key_of_must_copy = None
        if self._platforms.is_android_active():
            from build_android import AndroidBuilder
            key_of_copy = AndroidBuilder.CFG_KEY_COPY_TO_ASSETS
            key_of_must_copy = AndroidBuilder.CFG_KEY_MUST_COPY_TO_ASSERTS
        elif self._platforms.is_win32_active():
            key_of_copy = CCPluginCompile.CFG_KEY_WIN32_COPY_FILES
            key_of_must_copy = CCPluginCompile.CFG_KEY_WIN32_MUST_COPY_FILES

        if key_of_copy is None and key_of_must_copy is None:
            return

        try:
            outfile = None
            open_file = open(cfg_file_path)
            cfg_info = json.load(open_file)
            open_file.close()
            open_file = None
            changed = False
            if key_of_copy is not None:
                if cfg_info.has_key(key_of_copy):
                    src_list = cfg_info[key_of_copy]
                    ret_list = self._convert_cfg_list(src_list, build_cfg_dir)
                    cfg_info[CCPluginCompile.CFG_KEY_COPY_RESOURCES] = ret_list
                    del cfg_info[key_of_copy]
                    changed = True

            if key_of_must_copy is not None:
                if cfg_info.has_key(key_of_must_copy):
                    src_list = cfg_info[key_of_must_copy]
                    ret_list = self._convert_cfg_list(src_list, build_cfg_dir)
                    cfg_info[CCPluginCompile.CFG_KEY_MUST_COPY_RESOURCES] = ret_list
                    del cfg_info[key_of_must_copy]
                    changed = True

            if changed:
                # backup the old-cfg
                split_list = os.path.splitext(CCPluginCompile.BUILD_CONFIG_FILE)
                file_name = split_list[0]
                ext_name = split_list[1]
                bak_name = file_name + "-for-v0.1" + ext_name
                bak_file_path = os.path.join(build_cfg_dir, bak_name)
                if os.path.exists(bak_file_path):
                    os.remove(bak_file_path)
                os.rename(cfg_file_path, bak_file_path)

                # write the new data to file
                with open(cfg_file_path, 'w') as outfile:
                    json.dump(cfg_info, outfile, sort_keys = True, indent = 4)
                    outfile.close()
                    outfile = None
        finally:
            if open_file is not None:
                open_file.close()

            if outfile is not None:
                outfile.close()

    def _convert_cfg_list(self, src_list, build_cfg_dir):
        ret = []
        for element in src_list:
            ret_element = {}
            if str(element).endswith("/"):
                sub_str = element[0:len(element)-1]
                ret_element["from"] = sub_str
                ret_element["to"] = ""
            else:
                element_full_path = os.path.join(build_cfg_dir, element)
                if os.path.isfile(element_full_path):
                    to_dir = ""
                else:
                    to_dir = os.path.basename(element)
                ret_element["from"] = element
                ret_element["to"] = to_dir

            ret.append(ret_element)

        return ret

    def _is_debug_mode(self):
        return self._mode == 'debug'

    def _remove_file_with_ext(self, work_dir, ext):
        if not os.path.exists(work_dir):
           return
        file_list = os.listdir(work_dir)
        for f in file_list:
            full_path = os.path.join(work_dir, f)
            if os.path.isdir(full_path):
                self._remove_file_with_ext(full_path, ext)
            elif os.path.isfile(full_path):
                name, cur_ext = os.path.splitext(f)
                if cur_ext == ext:
                    os.remove(full_path)

    def compile_lua_scripts(self, src_dir, dst_dir, build_64):
        if not self._project._is_lua_project():
            return False

        if not self._compile_script and not self._lua_encrypt:
            return False

        cocos_cmd_path = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "cocos")
        rm_ext = ".lua"
        compile_cmd = "\"%s\" luacompile -s \"%s\" -d \"%s\"" % (cocos_cmd_path, src_dir, dst_dir)

        if not self._compile_script:
            compile_cmd = "%s --disable-compile" % compile_cmd
        elif build_64:
            compile_cmd = "%s --bytecode-64bit" % compile_cmd

        if self._lua_encrypt:
            add_para = ""
            if self._lua_encrypt_key is not None:
                add_para = "%s -k %s" % (add_para, self._lua_encrypt_key)

            if self._lua_encrypt_sign is not None:
                add_para = "%s -b %s" % (add_para, self._lua_encrypt_sign)

            compile_cmd = "%s -e %s" % (compile_cmd, add_para)

        # run compile command
        self._run_cmd(compile_cmd)

        # remove the source scripts
        self._remove_file_with_ext(dst_dir, rm_ext)

        return True

    def compile_js_scripts(self, src_dir, dst_dir):
        if not self._project._is_js_project():
            return False

        if not self._compile_script:
            return False

        cocos_cmd_path = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "cocos")
        rm_ext = ".js"
        compile_cmd = "\"%s\" jscompile -s \"%s\" -d \"%s\"" % (cocos_cmd_path, src_dir, dst_dir)

        # run compile command
        self._run_cmd(compile_cmd)

        # remove the source scripts
        # self._remove_file_with_ext(dst_dir, rm_ext)
        return True

    def add_warning_at_end(self, warning_str):
        if warning_str is None or len(warning_str) == 0:
            return
        self.end_warning = "%s\n%s" % (self.end_warning, warning_str)

    def is_valid_path(self, p):
        if (p is not None) and os.path.exists(p):
            ret = True
        else:
            ret = False

        return ret

    def get_engine_version_num(self):
        # 1. get engine version from .cocos_project.json
        engine_ver_str = self._project.get_proj_config(cocos_project.Project.KEY_ENGINE_VERSION)

        # 2. engine version is not found. find from source file
        if engine_ver_str is None:
            engine_dir = self.get_engine_dir()
            if engine_dir is not None:
                engine_ver_str = utils.get_engine_version(engine_dir)

        if engine_ver_str is None:
            return None

        version_pattern = r'cocos2d-x-([\d]+)\.([\d]+)'
        match = re.match(version_pattern, engine_ver_str)
        if match:
            return ((int)(match.group(1)), (int)(match.group(2)))
        else:
            return None

    def build_android(self):
        if not self._platforms.is_android_active():
            return

        project_dir = self._project.get_project_dir()
        build_mode = self._mode
        output_dir = self._output_dir

        # get the android project path
        cfg_obj = self._platforms.get_current_config()
        project_android_dir = cfg_obj.proj_path


        ide_name = 'Android Studio'
        cocos.Logging.info(MultiLanguage.get_string('COMPILE_INFO_ANDROID_PROJPATH_FMT', (ide_name, project_android_dir)))

        # Check whether the gradle of the project is support ndk or not
        # Get the engine version of the project
        engine_version_num = self.get_engine_version_num()
        if engine_version_num is None:
            raise cocos.CCPluginError(MultiLanguage.get_string('COMPILE_ERROR_UNKNOWN_ENGINE_VERSION'))

        # Gradle supports NDK build from engine 3.15
        main_ver = engine_version_num[0]
        minor_ver = engine_version_num[1]
        if main_ver > 3 or (main_ver == 3 and minor_ver >= 15):
            gradle_support_ndk = True
            

        from build_android import AndroidBuilder
        builder = AndroidBuilder(self._verbose, project_android_dir,
                                 self._no_res, self._project, self._mode, self._build_type,
                                 self.app_abi, gradle_support_ndk)

        args_ndk_copy = self._custom_step_args.copy()
        target_platform = self._platforms.get_current_platform()

        # update the project with the android platform
        builder.update_project(self._ap)

        if not self._project._is_script_project() or self._project._is_native_support():
            if self._build_type != "none" and not gradle_support_ndk:
                # build native code
                cocos.Logging.info(MultiLanguage.get_string('COMPILE_INFO_BUILD_NATIVE'))
                ndk_build_param = [
                    "-j%s" % self._jobs
                ]

                if self.app_abi:
                    abi_param = "APP_ABI=\"%s\"" % self.app_abi
                    ndk_build_param.append(abi_param)

                if self.ndk_toolchain:
                    toolchain_param = "NDK_TOOLCHAIN=%s" % self.ndk_toolchain
                    ndk_build_param.append(toolchain_param)

                self._project.invoke_custom_step_script(cocos_project.Project.CUSTOM_STEP_PRE_NDK_BUILD, target_platform, args_ndk_copy)

                modify_mk = False
                app_mk = os.path.join(project_android_dir, "app/jni/Application.mk")

                mk_content = None
                if self.cppflags and os.path.exists(app_mk):
                    # record the content of Application.mk
                    f = open(app_mk)
                    mk_content = f.read()
                    f.close()

                    # Add cpp flags
                    f = open(app_mk, "a")
                    f.write("\nAPP_CPPFLAGS += %s" % self.cppflags)
                    f.close()
                    modify_mk = True

                try:
                    builder.do_ndk_build(ndk_build_param, self._mode, self._build_type, self)
                except Exception as e:
                    if e.__class__.__name__ == 'CCPluginError':
                        raise e
                    else:
                        raise cocos.CCPluginError(MultiLanguage.get_string('COMPILE_ERROR_NDK_BUILD_FAILED'),
                                                  cocos.CCPluginError.ERROR_BUILD_FAILED)
                finally:
                    # roll-back the Application.mk
                    if modify_mk:
                        f = open(app_mk, "w")
                        f.write(mk_content)
                        f.close()

                self._project.invoke_custom_step_script(cocos_project.Project.CUSTOM_STEP_POST_NDK_BUILD, target_platform, args_ndk_copy)

        # build apk
        if not self._no_apk:
            cocos.Logging.info(MultiLanguage.get_string('COMPILE_INFO_BUILD_APK'))
        self.apk_path = builder.do_build_apk(build_mode, self._no_apk, self._no_sign, output_dir, self._custom_step_args, self._ap, self)
        self.android_package, self.android_activity = builder.get_apk_info()

        cocos.Logging.info(MultiLanguage.get_string('COMPILE_INFO_BUILD_SUCCEED'))

    def _remove_res(self, target_path):
        build_cfg_dir = self._build_cfg_path()
        cfg_file = os.path.join(build_cfg_dir, CCPluginCompile.BUILD_CONFIG_FILE)
        if os.path.exists(cfg_file) and os.path.isfile(cfg_file):
            # have config file
            open_file = open(cfg_file)
            cfg_info = json.load(open_file)
            open_file.close()
            if cfg_info.has_key("remove_res"):
                remove_list = cfg_info["remove_res"]
                for f in remove_list:
                    res = os.path.join(target_path, f)
                    if os.path.isdir(res):
                        # is a directory
                        if f.endswith('/'):
                            # remove files & dirs in it
                            for sub_file in os.listdir(res):
                                sub_file_fullpath = os.path.join(res, sub_file)
                                if os.path.isfile(sub_file_fullpath):
                                    os.remove(sub_file_fullpath)
                                elif os.path.isdir(sub_file_fullpath):
                                    shutil.rmtree(sub_file_fullpath)
                        else:
                            # remove the dir
                            shutil.rmtree(res)
                    elif os.path.isfile(res):
                        # is a file, remove it
                        os.remove(res)

    def get_engine_dir(self):
        engine_dir = self._project.get_proj_config(CCPluginCompile.PROJ_CFG_KEY_ENGINE_DIR)
        if engine_dir is None:
            proj_dir = self._project.get_project_dir()
            if self._project._is_js_project():
                check_dir = os.path.join(proj_dir, "frameworks", "cocos2d-x")
                if os.path.isdir(check_dir):
                    # the case for jsb in cocos2d-x engine
                    engine_dir = check_dir
                else:
                    # the case for jsb in cocos2d-js engine
                    engine_dir = proj_dir
            elif self._project._is_lua_project():
                engine_dir = os.path.join(proj_dir, "frameworks", "cocos2d-x")
            else:
                engine_dir = os.path.join(proj_dir, "cocos2d")
        else:
            engine_dir = os.path.join(self._project.get_project_dir(), engine_dir)

        return engine_dir

    def backup_dir(self, dir_path):
        backup_dir = "%s%s" % (dir_path, CCPluginCompile.BACKUP_SUFFIX)
        if os.path.exists(backup_dir):
            shutil.rmtree(backup_dir)
        shutil.copytree(dir_path, backup_dir)

    def reset_backup_dir(self, dir_path):
        backup_dir = "%s%s" % (dir_path, CCPluginCompile.BACKUP_SUFFIX)
        if os.path.exists(dir_path):
            shutil.rmtree(dir_path)
        os.rename(backup_dir, dir_path)

    def get_engine_js_dir(self):
        engine_js_dir = None
        isFound = False

        check_script_dir = os.path.join(self._project.get_project_dir(), "script")
        if os.path.isdir(check_script_dir):
            # JS script already copied into the project dir
            engine_js_dir = check_script_dir
            isFound = True
        else:
            for js_dir in CCPluginCompile.ENGINE_JS_DIRS:
                engine_js_dir = os.path.join(self.get_engine_dir(), js_dir)
                if os.path.isdir(engine_js_dir):
                    isFound = True
                    break

        if isFound:
            return engine_js_dir
        else:
            return None

    def _get_export_options_plist_path(self):
        project_dir = self._project.get_project_dir()

        possible_sub_paths = [ 'proj.ios', 'proj.ios_mac/ios', 'frameworks/runtime-src/proj.ios_mac/ios' ]
        ios_project_dir = None
        for sub_path in possible_sub_paths:
            ios_project_dir = os.path.join(project_dir, sub_path)
            if os.path.exists(ios_project_dir):
                break

        if ios_project_dir is None:
            return None

        return os.path.join(ios_project_dir, 'exportOptions.plist')

    def get_available_devenv(self, required_versions, min_ver, specify_vs_ver=None):
        if required_versions is None or len(required_versions) == 0:
            if specify_vs_ver is None:
                # Not specify VS version, find newest version
                needUpgrade, commandPath = utils.get_newest_devenv(min_ver)
            else:
                # Have specified VS version
                if specify_vs_ver < min_ver:
                    # Specified version is lower than required, raise error
                    raise cocos.CCPluginError(MultiLanguage.get_string('COMPILE_ERROR_LOW_VS_VER'),
                                              cocos.CCPluginError.ERROR_WRONG_ARGS)
                else:
                    # Get the specified VS
                    commandPath = utils.get_devenv_path(specify_vs_ver)
                    if specify_vs_ver > min_ver:
                        needUpgrade = True
                    else:
                        needUpgrade = False
        else:
            needUpgrade = False
            if specify_vs_ver is None:
                # find VS in required versions
                commandPath = None
                for v in required_versions:
                    commandPath = utils.get_devenv_path(v)
                    if commandPath is not None:
                        break
            else:
                # use specified VS version
                if specify_vs_ver in required_versions:
                    commandPath = utils.get_devenv_path(specify_vs_ver)
                else:
                    raise cocos.CCPluginError(MultiLanguage.get_string('COMPILE_ERROR_WRONG_VS_VER_FMT', specify_vs_ver),
                                              cocos.CCPluginError.ERROR_WRONG_ARGS)

        if commandPath is None:
            message = MultiLanguage.get_string('COMPILE_ERROR_VS_NOT_FOUND')
            raise cocos.CCPluginError(message, cocos.CCPluginError.ERROR_TOOLS_NOT_FOUND)

        return (needUpgrade, commandPath)

    def build_web(self):
        if not self._platforms.is_web_active():
            return

        project_dir = self._platforms.project_path()

        # store env for run
        cfg_obj = self._platforms.get_current_config()
        if cfg_obj.run_root_dir is not None:
            self.run_root = cfg_obj.run_root_dir
        else:
            self.run_root = project_dir

        if cfg_obj.sub_url is not None:
            self.sub_url = cfg_obj.sub_url
        else:
            self.sub_url = '/'

        output_dir = CCPluginCompile.OUTPUT_DIR_SCRIPT_RELEASE
        if self._is_debug_mode():
            output_dir = CCPluginCompile.OUTPUT_DIR_SCRIPT_DEBUG
            if not self._web_advanced:
                return

        self.sub_url = '%s%s/%s/' % (self.sub_url, output_dir, CCPluginCompile.WEB_PLATFORM_FOLDER_NAME)

        f = open(os.path.join(project_dir, "project.json"))
        project_json = json.load(f)
        f.close()
        engine_dir = os.path.join(project_json["engineDir"])
        realEngineDir = os.path.normpath(os.path.join(project_dir, engine_dir))
        publish_dir = os.path.normpath(os.path.join(project_dir, output_dir, CCPluginCompile.WEB_PLATFORM_FOLDER_NAME))

        # need to config in options of command
        buildOpt = {
                "outputFileName" : "game.min.js",
                "debug": "true" if self._is_debug_mode() else "false",
                "compilationLevel" : "advanced" if self._web_advanced else "simple",
                "sourceMapOpened" : True if self._has_sourcemap else False
                }

        if os.path.exists(publish_dir):
            shutil.rmtree(publish_dir)
        os.makedirs(publish_dir)

        # generate build.xml
        build_web.gen_buildxml(project_dir, project_json, publish_dir, buildOpt)

        outputJsPath = os.path.join(publish_dir, buildOpt["outputFileName"])
        if os.path.exists(outputJsPath) == True:
            os.remove(outputJsPath)


        # call closure compiler
        ant_root = cocos.check_environment_variable('ANT_ROOT')
        ant_path = os.path.join(ant_root, 'ant')
        self._run_cmd("%s -f %s" % (ant_path, os.path.join(publish_dir, 'build.xml')))

        # handle sourceMap
        sourceMapPath = os.path.join(publish_dir, "sourcemap")
        if os.path.exists(sourceMapPath):
            smFile = open(sourceMapPath)
            try:
                smContent = smFile.read()
            finally:
                smFile.close()

            dir_to_replace = project_dir
            if cocos.os_is_win32():
                dir_to_replace = project_dir.replace('\\', '\\\\')
            smContent = smContent.replace(dir_to_replace, os.path.relpath(project_dir, publish_dir))
            smContent = smContent.replace(realEngineDir, os.path.relpath(realEngineDir, publish_dir))
            smContent = smContent.replace('\\\\', '/')
            smContent = smContent.replace('\\', '/')
            smFile = open(sourceMapPath, "w")
            smFile.write(smContent)
            smFile.close()

        # handle project.json
        del project_json["engineDir"]
        del project_json["modules"]
        del project_json["jsList"]
        project_json_output_file = open(os.path.join(publish_dir, "project.json"), "w")
        project_json_output_file.write(json.dumps(project_json))
        project_json_output_file.close()

        # handle index.html
        indexHtmlFile = open(os.path.join(project_dir, "index.html"))
        try:
            indexContent = indexHtmlFile.read()
        finally:
            indexHtmlFile.close()
        reg1 = re.compile(r'<script\s+src\s*=\s*("|\')[^"\']*CCBoot\.js("|\')\s*><\/script>')
        indexContent = reg1.sub("", indexContent)
        mainJs = project_json.get("main") or "main.js"
        indexContent = indexContent.replace(mainJs, buildOpt["outputFileName"])
        indexHtmlOutputFile = open(os.path.join(publish_dir, "index.html"), "w")
        indexHtmlOutputFile.write(indexContent)
        indexHtmlOutputFile.close()
        
        # copy res dir
        if cfg_obj.copy_res is None:
            dst_dir = os.path.join(publish_dir, 'res')
            src_dir = os.path.join(project_dir, 'res')
            if os.path.exists(dst_dir):
                shutil.rmtree(dst_dir)
            shutil.copytree(src_dir, dst_dir)
        else:
            for cfg in cfg_obj.copy_res:
                cocos.copy_files_with_config(cfg, project_dir, publish_dir)

        # copy to the output directory if necessary
        pub_dir = os.path.normcase(publish_dir)
        out_dir = os.path.normcase(os.path.normpath(self._output_dir))
        if pub_dir != out_dir:
            cpy_cfg = {
                "from" : pub_dir,
                "to" : out_dir
            }
            cocos.copy_files_with_config(cpy_cfg, pub_dir, out_dir)

    def check_platform(self, platform):
        if platform == 'mac':
            if not self._platforms.is_mac_active() or not cocos.os_is_mac:
                raise cocos.CCPluginError(MultiLanguage.get_string('COMPILE_ERROR_BUILD_ON_MAC'),
                                          cocos.CCPluginError.ERROR_WRONG_ARGS)

        if platform == 'ios':
            if not self._platforms.is_ios_active() or not cocos.os_is_mac:
                raise cocos.CCPluginError(MultiLanguage.get_string('COMPILE_ERROR_BUILD_ON_MAC'),
                                          cocos.CCPluginError.ERROR_WRONG_ARGS)

        if platform == 'win32':
            if not self._platforms.is_win32_active or not cocos.os_is_win32:
                raise cocos.CCPluginError(MultiLanguage.get_string('COMPILE_ERROR_BUILD_ON_WIN'),
                                          cocos.CCPluginError.ERROR_WRONG_ARGS)
        
        if platform == 'linux':
            if not self._platforms.is_linux_active():
                raise cocos.CCPluginError("Please build on linux")

    def compile_script(self, script_path, platform):
        """
        Compile and encrypt script files if needed.
        """

        if not self._project._is_script_project:
            return

        if self._project._is_lua_project:
            build_64bit = True if platform != 'win32' else False
            build_32bit = True if platform != 'mac' else False

            if build_64bit:
                folder_64bit = os.path.join(script_path, '64bit')
                self.compile_lua_scripts(script_path, folder_64bit, True)

            if build_32bit:
                self.compile_lua_scripts(script_path, script_path, False) 

            # mac only support 64bit, so should remove .lua files not int folder_64bit
            if platform == 'mac' and self._compile_script:
                self._remove_file_with_ext(script_path, '.lua')
        
        # self.compile_js_script(path)

        # if only support 64bit, then move to

    def build(self, platform):
        if self._platforms.is_android_active():
            self.build_android()
            return

        self.check_platform(platform)

        project_dir = self._project.get_project_dir()
        cfg_obj = self._platforms.get_current_config()
        if cfg_obj.cmake_path is not None:
            cmakefile_dir = os.path.join(project_dir, cfg_obj.cmake_path)
        else:
            cmakefile_dir = project_dir

        # get the project name
        if cfg_obj.project_name is not None:
            self.project_name = cfg_obj.project_name
        else:
            f = open(os.path.join(cmakefile_dir, 'CMakeLists.txt'), 'r')
            regexp_set_app_name = re.compile(r'\s*set\s*\(\s*APP_NAME', re.IGNORECASE)
            for line in f.readlines():
                if regexp_set_app_name.search(line):
                    self.project_name = re.search('APP_NAME ([^\)]+)\)', line, re.IGNORECASE).group(1)
                    break
            if hasattr(self, 'project_name') == False:
	            raise cocos.CCPluginError("Couldn't find APP_NAME in CMakeLists.txt")

        if cfg_obj.build_dir is not None:
            build_dir = os.path.join(project_dir, cfg_obj.build_dir)
        else:
            build_dir = os.path.join(project_dir, '%s-build' % platform)

        if not os.path.exists(build_dir):
            os.makedirs(build_dir)

        # compile codes
        build_mode = 'Debug' if self._is_debug_mode() else 'Release'
        with cocos.pushd(build_dir):
            # iOS need to generate Xcode project file first
            if platform == 'ios':
                engine_dir = self.get_engine_dir()
                self._run_cmd('cmake %s -GXcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_SYSROOT=%s' % 
                              ( os.path.relpath(cmakefile_dir, build_dir), self._use_sdk ) )
            elif platform == 'mac':
                self._run_cmd('cmake -GXcode %s' % os.path.relpath(cmakefile_dir, build_dir))
            elif platform == "win32":
                ret = utils.get_newest_devenv(self.vs_version)
                if ret is not None:
                    ver_num = int(float(ret[2]))
                    generator = self.CMAKE_VS_GENERATOR_MAP[str(ver_num)]
                    if generator is not None:
                        if ver_num >= 16:
                            # for vs2019 x64 is the default target
                            self._run_cmd('cmake %s -G "%s" -A win32' % 
                                (os.path.relpath(cmakefile_dir, build_dir), generator))
                        else: 
                            self._run_cmd('cmake %s -G "%s"' % 
                                (os.path.relpath(cmakefile_dir, build_dir), generator))
                    else:
                        cocos.Logging.warning(MultiLanguage.get_string("COMPILE_VS_VERSION_NOT_REGISTER") % (ret[2]))
                        self._run_cmd('cmake %s' % os.path.relpath(cmakefile_dir, build_dir) )
                else:
                    cocos.Logging.warning(MultiLanguage.get_string("COMPILE_VS_VERSION"))
                    self._run_cmd('cmake %s' % os.path.relpath(cmakefile_dir, build_dir) )
            else:
                self._run_cmd('cmake %s' % os.path.relpath(cmakefile_dir, build_dir) )

            self._run_cmd('cmake --build . --config %s' % build_mode)

        # move file
        output_dir = self._output_dir

        if os.path.exists(output_dir):
            shutil.rmtree(output_dir)
        os.makedirs(output_dir)

        if cfg_obj.build_result_dir is not None:
            result_dir = os.path.join(build_dir, 'bin', cfg_obj.build_result_dir, self.project_name)
        else:
            result_dir = os.path.join(build_dir, 'bin', self.project_name)

        if os.path.exists(os.path.join(result_dir, build_mode)):
            result_dir = os.path.join(result_dir, build_mode)

        cocos.copy_files_in_dir(result_dir, output_dir)

        self.run_root = output_dir

        # set application path and application name
        if platform == 'mac' or platform == 'ios':
            self.app_path = os.path.join(output_dir, self.project_name + '.app')
        else:
            self.app_path = output_dir

        self.app_name = self.project_name
        if platform == 'win32':
            self.app_name = self.app_name + '.exe'

        script_resource_path = os.path.join(self.app_path, 'src')
        if platform == 'mac':
            script_resource_path = os.path.join(self.app_path, 'Contents/Resources/src')

        cocos.Logging.info(MultiLanguage.get_string('COMPILE_INFO_BUILD_SUCCEED'))

    def _get_build_cfg(self):
        build_cfg_dir = self._build_cfg_path()
        build_cfg = os.path.join(build_cfg_dir, CCPluginCompile.BUILD_CONFIG_FILE)
        if not os.path.exists(build_cfg):
            message = MultiLanguage.get_string('COMPILE_ERROR_FILE_NOT_FOUND_FMT', build_cfg)
            raise cocos.CCPluginError(message, cocos.CCPluginError.ERROR_PATH_NOT_FOUND)
        f = open(build_cfg)
        return json.load(f)

    def _copy_resources(self, dst_path):
        data = self._get_build_cfg()

        if data.has_key(CCPluginCompile.CFG_KEY_MUST_COPY_RESOURCES):
            if self._no_res:
                fileList = data[CCPluginCompile.CFG_KEY_MUST_COPY_RESOURCES]
            else:
                fileList = data[CCPluginCompile.CFG_KEY_COPY_RESOURCES] + data[CCPluginCompile.CFG_KEY_MUST_COPY_RESOURCES]
        else:
            fileList = data[CCPluginCompile.CFG_KEY_COPY_RESOURCES]

        for cfg in fileList:
            cocos.copy_files_with_config(cfg, self._build_cfg_path(), dst_path)

    def checkFileByExtention(self, ext, path):
        filelist = os.listdir(path)
        for fullname in filelist:
            name, extention = os.path.splitext(fullname)
            if extention == ext:
                return name, fullname
        return (None, None)

    def run(self, argv, dependencies):
        self.parse_args(argv)
        cocos.Logging.info(MultiLanguage.get_string('COMPILE_INFO_BUILD_MODE_FMT', self._mode))
        self._update_build_cfg()

        target_platform = self._platforms.get_current_platform()
        args_build_copy = self._custom_step_args.copy()

        language = self._project.get_language()
        action_str = 'compile_%s' % language
        target_str = 'compile_for_%s' % target_platform
        cocos.DataStatistic.stat_event('compile', action_str, target_str)

        # invoke the custom step: pre-build
        self._project.invoke_custom_step_script(cocos_project.Project.CUSTOM_STEP_PRE_BUILD, target_platform, args_build_copy)

        self.build_web()
        self.build(target_platform)

        # invoke the custom step: post-build
        self._project.invoke_custom_step_script(cocos_project.Project.CUSTOM_STEP_POST_BUILD, target_platform, args_build_copy)

        if len(self.end_warning) > 0:
            cocos.Logging.warning(self.end_warning)