#!/usr/bin/env python3 # -*- coding: utf-8; mode: python; tab-width: 4; indent-tabs-mode: nil -*- # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 # # THIS FILE IS PART OF adtools PROJECT # # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # Version 2, December 2004 # # Copyright (C) 2004 Sam Hocevar # # Everyone is permitted to copy and distribute verbatim or modified # copies of this license document, and changing it is allowed as long # as the name is changed. # # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION # # 0. You just DO WHAT THE FUCK YOU WANT TO. import os import sys import argparse import textwrap import re import inspect import traceback import glob import crayons from .__init__ import __version__ from .adutil import AdUtil, Logger, WorkspaceHelper, TripletHelper, W, E, D from .adgit import AdGitHelper SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) class VersionSourceWrapper: DEFAULT_VERSION_FILE = "version" def __init__(self, args): self._args = args self._file_content = None self._file_path = None def __enter__(self): if not self._args.gen_version: return else: gh = AdGitHelper() file_name = os.path.join( "source", "{}.cpp".format(VersionSourceWrapper.DEFAULT_VERSION_FILE) ) self._file_path = os.path.join(self._args.dir, file_name) if not os.path.isfile(self._file_path): W( crayons.yellow( f"The required version definition file is missing: <{self._file_path}>" ) ) return version_info = gh.get_revision(cur_dir=self._args.dir) if not version_info: W( crayons.yellow( f"Failed to generate the version string for <{self._file_path}>" ) ) return if "short_commit" not in version_info: ver = f"{version_info['version']}-({version_info['commit']})" else: ver = f"{version_info['version']}-({version_info['short_commit']})" self._file_content = open(self._file_path).read() m = re.sub( r"(.*)std::string\s+VersionString(.*)", rf'\1std::string VersionString = "{ver}";', self._file_content, ) open(self._file_path, "w+").write(m) def __exit__(self, exc_type, exc_val, traceback): # Recover the file content if self._file_content: open(self._file_path, "w+").write(self._file_content) return True class Command: def __init__(self): self.ws_helper = WorkspaceHelper() self.__build_dir_name = ".build" self.__package_dir_name = ".package" def help(self, *args) -> int: """Shows help for a specific command""" parser = argparse.ArgumentParser( description=self.help.__doc__, prog="admake help" ) parser.add_argument("command", help="command", nargs="?") args = parser.parse_args(*args) if not args.command: self._show_help() return cmds = self._commands() if args.command in cmds: return cmds[args.command](["--help"]) else: W(crayons.yellow(f"Command <{args.command}> is unsupported!")) return -1 def _get_build_dirname(self, args): if not args.platform: args.platform = AdUtil.arch() base_dir = f"{args.os}_{args.platform}" build_dir = "{}{}{}".format( base_dir, "_release" if args.release else "_debug", "_test" if args.build_test else "", ) return build_dir def _build_dir(self, args): build_dir = self._get_build_dirname(args) build_dir = os.path.join(self.__build_dir_name, build_dir) return os.path.join(os.path.abspath(args.dir), build_dir) def _pkg_dir(self, args): return os.path.join(os.path.abspath(args.dir), self.__package_dir_name).replace( "\\", "/" ) def _construct_cmake_macros(self, unknown_args: list) -> list: # Construct unknown arguments as -D options for CMake other_cmake_macros = [] unknown_args_len = len(unknown_args) if unknown_args_len > 0: idx = 0 while idx < unknown_args_len: cur_arg = unknown_args[idx].strip() if cur_arg.startswith("--") and len(cur_arg) > 2 and cur_arg[2] != "-": cur_arg = cur_arg[2:].replace("-", "_").upper() opt = f"-DADMAKE_{cur_arg}" if idx + 1 < unknown_args_len: opt_cur_arg = unknown_args[idx + 1].strip() if not opt_cur_arg.startwith("-"): opt += f'="{opt_cur_arg}"' idx += 1 else: opt += ":BOOL=ON" else: opt += ":BOOL=ON" other_cmake_macros.append(opt) idx += 1 return other_cmake_macros def _do_build(self, cmd_name: str = "build", *args_) -> int: """ Do build for build and rebuild """ parser = argparse.ArgumentParser( description=self.build.__doc__ if cmd_name == "build" else self.rebuild.__doc__, prog=f"admake {cmd_name}", ) # TODO(donkey): *BSD, ChromeOS, iOS parser.add_argument( "-d", "--dir", default=".", help="Specify the directory contains the CMakeLists.txt", ) parser.add_argument( "-v", "--verbose", action="store_true", default=False, help="Output the verbose information in the building progress", ) parser.add_argument( "-o", "--os", default=AdUtil.SYSTEM.lower(), help="Target operating system. Supported operating systems : {}".format( ", ".join( ["windows", "linux", "android", "qnx", "darwin", "none", "unknonw"] ) ), ) parser.add_argument( "-p", "--platform", default="", help="The default value is different in different OSs.\n", ) parser.add_argument( "-t", "--build-test", action="store_true", help="Build the test project with the static library.", ) parser.add_argument( "-c", "--cc", default="", help="Specify the default compiling toolchain, such as: gcc, gcc-7, llvm, llvm-14, msvc, msvc-vs2019", ) parser.add_argument( "--ccjson", action="store_true", help="Generate compile_commands.json in the project root directory.", ) parser.add_argument( "-a", "--toolchain", default="", help="Specify the CMake TOOLCHAIN_FILE_PATH, if this option only contains a file name, admake will search it in CMAKE_MODULE_PATH.", ) parser.add_argument( "-D", default=[], action="append", help="-D argument pass to CMake, e.g: -DENABLE_LOG -DTEST_BUILD=1", ) parser.add_argument( "-r", "--release", action="store_true", help="Build release edition" ) parser.add_argument( "-m", "--macro", default=[], action="append", help="C/C++ macros pass to compilers, e.g: -m BUILD_UNIT_TEST", ) parser.add_argument( "-g", "--gen-version", action="store_true", default=False, help="Attempt to generate version information", ) parser.add_argument( "--cmake-generator", default="", help="Specify the cmake generator, used by automatical derivation", ) args, unknown_args = parser.parse_known_args(*args_) if args.verbose: Logger.get_instance().enable() args.dir = os.path.abspath(args.dir) with AdUtil.chdir(args.dir): if not os.path.isfile("CMakeLists.txt"): E(crayons.red("Required CMakeLists.txt is not existent!")) return -1 other_cmake_macros = self._construct_cmake_macros(unknown_args) other_cmake_macros.extend([f"-D{d}" for d in args.D]) th = TripletHelper(self.ws_helper, args.dir) tool = th.load( arch=args.platform, os=args.os, cc=args.cc, cmg=args.cmake_generator, toolchain=args.toolchain, ) if not tool: E(crayons.red("Failed to load toolchain.")) return -1 build_dir = self._build_dir(args) cfg = "Release" if args.release else "Debug" rebuild = cmd_name == "rebuild" with AdUtil.chdir(args.dir): if rebuild: AdUtil.remove(build_dir) AdUtil.mkdirs(build_dir) cmake_macros_list = [ f'-DADMAKE_BUILD_DIR="{build_dir}"', "-DADMAKE_BUILD_TEST:BOOL=ON" if args.build_test else "", f'-DADMAKE_OS="{args.os}"', f'-DADMAKE_PLATFORM="{args.platform}"', ] cmake_macros_list.extend(other_cmake_macros) cmake_macros = " ".join(cmake_macros_list) args.macro.append(f"OS_{args.os.upper()}") lang_macros = '-DCUSTOM_MACROS:LIST="{}"'.format(";".join(args.macro)) pkg_dir = self._pkg_dir(args) cmake_cmd_list = [ f'cmake --no-warn-unused-cli -S "{args.dir}"', f'-B "{build_dir}"', f'-DCMAKE_INSTALL_PREFIX="{pkg_dir}"', "-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON" if args.verbose else "", "-DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=ON" if args.ccjson else "", f"-DCMAKE_BUILD_TYPE={cfg}", '-DCMAKE_MODULE_PATH="{}/cmake"'.format(SCRIPT_DIR.replace("\\", "/")), cmake_macros, lang_macros, tool.additional, f'-G "{tool.generator}"', ] with VersionSourceWrapper(args): # Run `cmake` code, msg = AdUtil.run_command(cmake_cmd_list) if code != 0: E(crayons.red(f"Failed to call cmake\n{msg}")) return code else: # Run `make` code, msg = AdUtil.run_command(tool.make_command(build_dir, cfg)) if code != 0: E(crayons.red(f"Failed to build target\n{msg}")) return code # For some editors, compile_commands.json is very useful, # so we attempt to create symbolic to compile_commands.json # if it exists. ccjson_path = os.path.join(build_dir, "compile_commands.json") if os.path.isfile(ccjson_path): # Delete the old symbolic link before create new one oldone = os.path.join(args.dir, "compile_commands.json") if os.path.exists(oldone): AdUtil.remove(oldone) AdUtil.mklink(ccjson_path, oldone) D( crayons.blue( f"Create symbolic link compile_commands.json -> {ccjson_path}" ) ) return 0 def build(self, *args) -> int: """ Build current project with the specific toolchain. """ return self._do_build("build", *args) def coverage(self, *args_) -> int: """ Generate coverage report for this project """ parser = argparse.ArgumentParser( description=self.coverage.__doc__, prog=f"admake coverage" ) parser.add_argument( "-d", "--dir", default=".", help="Specify the directory contains the CMakeLists.txt", ) parser.add_argument( "-v", "--verbose", action="store_true", default=False, help="Output the verbose information in the building progress", ) parser.add_argument( "-q", "--quiet", action="store_true", default=False, help="Do not print progress messages", ) parser.add_argument( "-s", "--source", action="store_true", default=False, help="Generate the source pages", ) parser.add_argument( "-o", "--os", default=AdUtil.SYSTEM.lower(), help="Target operating system. Supported operating systems : {}".format( ", ".join( ["windows", "linux", "android", "qnx", "darwin", "none", "unknonw"] ) ), ) parser.add_argument( "-p", "--platform", default="", help="The default value is different in different OSs.\n", ) parser.add_argument( "-t", "--build-test", action="store_true", help="For the test project." ) parser.add_argument( "-r", "--release", action="store_true", help="For the release version" ) parser.add_argument( "--output", default="coverage-report", help="The output directory of the coverage-report, default: /coverage-report", ) args, unknown_args = parser.parse_known_args(*args_) if args.verbose: Logger.get_instance().enable() # TODO: LLVM support # Check programs for p in ["lcov", "genhtml"]: if not AdUtil.command_exists(p): print(crayons.red(f"The required command {p} is not existent!")) return -1 args.dir = os.path.abspath(args.dir) build_dir = self._build_dir(args) title = os.path.basename(args.dir) with AdUtil.chdir(args.dir): output_dir = os.path.abspath(args.output) AdUtil.mkdirs(output_dir) cmd_list = [ "lcov", "--quiet" if args.quiet else "", "--rc lcov_branch_coverage=1", "--no-external", f"--directory {build_dir}", f"--base-directory {args.dir}", # Ignore all the unit test cases "--exclude '*test/*'", "--capture", f"--output-file {output_dir}/{title}.info", ] code, r = AdUtil.run_command(cmd_list, True) if code != 0: print(crayons.red(f"Failed to run command lcov: {r}")) return -1 cmd_list = [ "genhtml", "--no-source" if not args.source else "", "--quiet" if args.quiet else "", "--branch-coverage", "--demangle-cpp", "--sort", "--highlight", "--legend", f"--title '{title}'", f"-o {output_dir}", f"{output_dir}/{title}.info", ] code, r = AdUtil.run_command(cmd_list) if code != 0: print(crayons.red(f"Failed to run command genhtml: {r}")) return -1 return 0 def rebuild(self, *args) -> int: """ Rebuild the project """ return self._do_build("rebuild", *args) def export(self, *args_) -> int: """ Export current project as an independent project """ parser = argparse.ArgumentParser( description=self.export.__doc__, prog="admake export" ) parser.add_argument( "-d", "--dir", default=".", help="Specify the project root directory" ) args = parser.parse_args(*args_) # Collect all the dependencies, pack as thirdparty libraries return 0 def publish(self, *args_) -> int: """ Publish the project to a specific NEXUS 3 server """ parser = argparse.ArgumentParser( description=self.export.__doc__, prog="admake publish" ) parser.add_argument( "-d", "--dir", default=".", help="Specify the directory contains the CMakeLists.txt.", ) parser.add_argument( "-n", "--name", help="Specify the output name of your package." ) parser.add_argument( "-o", "--os", default=AdUtil.SYSTEM.lower(), help="Target operating system. Supported operating systems : {}".format( ", ".join( ["windows", "linux", "android", "qnx", "darwin", "none", "unknonw"] ) ), ) parser.add_argument( "-p", "--platform", default="", help="The default value is different in different OSs.\n", ) parser.add_argument( "-r", "--release", action="store_true", help="Build release edition" ) parser.add_argument( "-t", "--build-test", action="store_true", help="Build the test project with the static library.", ) parser.add_argument( "-l", "--other", default="", help="Publish to the local 3rd-party repository with a specific name.", ) parser.add_argument( "-m", "--module", default="", help="Publish to the local module repository with a specific name.", ) args = parser.parse_args(*args_) args.dir = os.path.abspath(args.dir) build_dir = self._build_dir(args).replace("\\", "/") cfg = "Release" if args.release else "Debug" # The same as "cmake --install --config " code, msg = AdUtil.run_command( f'cmake --build "{build_dir}" --target install --config {cfg}' ) if code != 0: E(crayons.red(f"Failed to run the install target.\n{msg}")) return -1 pkg_dir = self._pkg_dir(args) if args.module: if self.ws_helper.add_to_ws(path=pkg_dir, alias=args.module): print(crayons.green(f"Append {args.module} to workspace")) else: print(crayons.red(f"Failed to append {args.other} to workspace")) if args.other: if self.ws_helper.add_to_thirdparty(pkg_dir, alias=args.other): print(crayons.green(f"Append {args.other} to 3rd-party")) else: print(crayons.red(f"Failed to append {args.other} to 3rd-party")) # Publish the library to NEXUS3 return 0 def ws(self, *args_) -> int: """ Manages a workspace (a set of packages consumed from the user workspace that belongs to the same project). """ parser = argparse.ArgumentParser(description=self.ws.__doc__, prog="admake ws") parser.add_argument( "-l", "--list", action="store_true", help="List all the available workspace" ) parser.add_argument( "-x", "--switch", default="", help="Switch to specific workspace" ) parser.add_argument("-c", "--create", nargs="+", help="Create a new workspace") parser.add_argument( "-r", "--remove", nargs="+", help="Remove the specific workspace" ) parser.add_argument("-d", "--clear", nargs="+", help="Clear the workspace") parser.add_argument( "-t", "--tree", action="store_true", help="List all the modules in CURRENT workspace", ) args = parser.parse_args(*args_) if len(*args_) == 0: parser.print_help() return 0 if args.list: cur_ws_name = self.ws_helper.current_ws_name lw = self.ws_helper.list_all() print(crayons.blue("All available workspaces:")) for name in lw: if name == cur_ws_name: print(f" {name}", crayons.green("(CURRENT)")) else: print(f" {name}") return 0 if args.switch: self.ws_helper.switchto(args.switch) return if args.tree: print(crayons.blue("Modules in CURRENT workspace:")) for m in self.ws_helper.list_children(): print(crayons.green(f" {m}")) return if args.clear: for name in args.clear: self.ws_helper.clear_ws(name) if args.create: for name in args.create: self.ws_helper.create_ws(name) if args.remove: for name in args.remove: self.ws_helper.remove_ws(name) return 0 def mark(self, *args_) -> int: """ Modules and Thirdparty packages manager """ parser = argparse.ArgumentParser( description=self.mark.__doc__, prog="admake mark" ) parser.add_argument( "-w", "--ws", default="", help="Specify the target workspace, default: CURRENT", ) parser.add_argument( "-l", "--list", action="store_true", help="List all the modules in specific workspace and 3rd-party", ) parser.add_argument( "-d", "--directory", nargs="*", action="append", help="Specify the directory which sub-directories will should be marked as admake modules.", ) parser.add_argument( "-m", "--module", nargs="*", action="append", help="Specify the directories which will should be marked as admake modules", ) parser.add_argument( "-o", "--other", nargs="*", action="append", help="Specify the directories which will should be marked as admake 3rd-party modules", ) parser.add_argument( "-x", "--remove", action="store_true", help="Remove existent admake modules/3rd-party modules", ) args = parser.parse_args(*args_) if len(*args_) == 0: parser.print_help() return 0 if args.remove and args.directory: print( crayons.red( "[ERROR] --remove and --directory can not be set at the same time." ) ) return -1 ws_name = self.ws_helper.current_ws_name if args.ws: ws_name = args.ws if args.list: print( crayons.green("Modules in"), crayons.blue(ws_name), crayons.green("workspace:"), ) for m in self.ws_helper.list_children(args.ws): print(crayons.blue(f" {m}")) print(crayons.cyan("\nTHirdparty modules:")) for m in self.ws_helper.list_thirdparty(): print(crayons.green(f" {m}")) return 0 modules = [] if args.directory: for sdir in args.directory: for sub_dir in sdir: sub_dir = os.path.abspath(sub_dir) if not os.path.isdir(sub_dir): continue for ssdir in os.listdir(sub_dir): absdir = os.path.abspath(os.path.join(sub_dir, ssdir)) if not os.path.isdir(absdir): continue modules.append(absdir) if args.module: for sub_mod in args.module: modules.extend(sub_mod) others = [] if args.other: for sub_mod in args.other: others.extend(sub_mod) if args.remove: for m in modules: self.ws_helper.remove_from_ws(m, args.ws) print( crayons.yellow("Remove module"), crayons.green(os.path.basename(m)), crayons.yellow("from"), crayons.cyan(ws_name), ) for m in others: self.ws_helper.remove_from_thirdparty(m) else: for m in modules: self.ws_helper.add_to_ws(m, args.ws) print( crayons.blue("Add module"), crayons.green(os.path.basename(m)), crayons.blue("to"), crayons.cyan(ws_name), ) for m in others: self.ws_helper.add_to_thirdparty(m) return 0 def update(self, args_) -> int: """ Update the depandences marked in CMakeLists.txt for current project. """ parser = argparse.ArgumentParser( description=self.update.__doc__, prog="admake update" ) parser.add_argument( "-d", "--dir", default=".", help="Specify the directory contains the CMakeLists.txt", ) args = parser.parse_args(*args_) mh = AdMakeHelper(args.dir) gh = AdGitHelper() group_dir = self.ws_helper.current_ws_dir projects = mh.modules() projs_num = len(projects) projs_bit_num = len(str(projs_num)) i = 0 for p in projects: i += 1 p_path = os.path.join(group_dir, p) print( crayons.yellow("[{:0>{}d}/{}] ".format(i, projs_bit_num, projs_num)), end="", ) if os.path.exists(p_path): with AdUtil.chdir(p_path): print(crayons.green("Updating project: {: <20} ".format(p))) if gh.pull()[0] == 0: # If successfully, remove the built artifacts # clean_dist(fake_args, None) pass else: print(crayons.blue("Skipped, CMake will clone the project")) return 0 def clean(self, *args_) -> int: """ Cleanup the project for rebuilding. """ parser = argparse.ArgumentParser( description=self.clean.__doc__, prog="admake clean" ) parser.add_argument( "-a", "--all", action="store_true", help="Remove all the artifacts, it is DANGEROUS!", ) parser.add_argument( "-o", "--os", default=AdUtil.SYSTEM.lower(), choices=("windows", "linux", "qnx", "macos", "none"), help="Target operating system. Supported operating systems : {}".format( ", ".join(["windows", "linux", "qnx", "macos", "none"]) ), ) parser.add_argument( "-p", "--platform", choices=("x64", "arm64"), default="x64", help="The default value is different in different OSs.\n", ) parser.add_argument( "-r", "--release", action="store_true", help="Clean up the release edition" ) parser.add_argument( "-t", "--build-test", action="store_true", help="Build the test project with the static library.", ) args = parser.parse_args(*args_) if args.all: for d in ["lib", "bin", self.__build_dir_name]: AdUtil.remove(d) else: AdUtil.remove( os.path.join(self.__build_dir_name, self._get_build_dirname(args)) ) return 0 def config(self, *args_) -> int: """ Output some config information to stdout, so you can use it in scripts. """ parser = argparse.ArgumentParser( description=self.clean.__doc__, prog="admake dirs" ) parser.add_argument( "-c", "--cmake", action="store_true", help="Output the additional CMAKE_MOUDLE_PATH from admake",) parser.add_argument( "-d", "--docker", const=":", default="", type=str, nargs="?", help="Specify the docker container that you want to run, :",) args = parser.parse_args(*args_) if args.cmake: print(os.path.join(SCRIPT_DIR, "cmake")) if args.docker: if ":" == args.docker: # Default name args.docker = "admake-cross-linux-x64:latest" tpl = ['docker run -it --rm --net=host', '-v `dirname ${HOME}/.admake`:/home/admake/.admake', '-v `dirname ${PWD}`:/workspace', '-w /workspace/`basename ${PWD}`', '--name build_{}'.format(AdUtil.random_str()), '-u $(id -u ${USER}):$(id -g ${USER})', '-v "/etc/passwd:/etc/passwd"', '-v "/etc/group:/etc/group"', '-e HOME="/home/admake"', '-e NEXUS_HOST=${NEXUS_HOST}', '-e NEXUS_USERNAME=${NEXUS_USERNAME}', '-e NEXUS_PASSWORD=${NEXUS_PASSWORD}', args.docker, '/bin/bash'] print(' '.join(tpl)) return 0 def _show_help(self): """ Prints a summary of all commands """ cmds = [ "build", "rebuild", "coverage", "export", "publish", "ws", "mark", "clean", "config", ] max_len = max((len(c) for c in cmds)) + 1 commands = self._commands() for name in cmds: print(crayons.green(name), end="") # Help will be all the lines up to the first empty one docstring_lines = commands[name].__doc__.split("\n") start = False data = [] for line in docstring_lines: line = line.strip() if not line: if start: break start = True continue data.append(line) txt = (" " * (max_len + 2 - len(name))) + textwrap.fill( " ".join(data), 80, subsequent_indent=" " * (max_len + 2) ) print(txt) def _commands(self) -> dict: """Returns a list of available commands.""" result = {} for m in inspect.getmembers(self, predicate=inspect.ismethod): method_name = m[0] if not method_name.startswith("_"): method = m[1] if method.__doc__ and not method.__doc__.startswith("HIDDEN"): result[method_name] = method return result def run(self, *args) -> int: """HIDDEN: entry point for executing commands, dispatcher to class methods""" ret_code = 0 try: try: command = args[0][1] except IndexError: # No parameters self._show_help() return 0 cmds = self._commands() if command in cmds: ret_code = cmds[command](args[0][2:]) else: if command in ["-v", "--version"]: print(__version__) return 0 if command in ["-h", "--help"]: self._show_help() return 0 except Exception: print(traceback.format_exc()) return -1 return ret_code if __name__ == "__main__": cmd = Command() sys.exit(cmd.run(sys.argv))