#!/usr/bin/env python3
import datetime
import argparse
import subprocess
import os
import sys
import shutil
from collections.abc import Callable
from dataclasses import dataclass

# constants
VERSION = '4.0'
TAG_TIME = '2026-2-8'

# color escape code definition
if not os.getenv('NO_COLOR'):
    def TXT_RED(s): return f'\x1b[31m{s}\x1b[0m'
    def TXT_GREEN(s): return f'\x1b[32m{s}\x1b[0m'
    def TXT_YELLOW(s): return f'\x1b[33m{s}\x1b[0m'
    def TXT_BLUE(s): return f'\x1b[34m{s}\x1b[0m'
    def TXT_MAGENTA(s): return f'\x1b[35m{s}\x1b[0m'
    def TXT_CYAN(s): return f'\x1b[36m{s}\x1b[0m'
    def TXT_GRAY(s): return f'\x1b[90m{s}\x1b[0m'
else:
    def TXT_RED(s): return s
    def TXT_GREEN(s): return s
    def TXT_YELLOW(s): return s
    def TXT_BLUE(s): return s
    def TXT_MAGENTA(s): return s
    def TXT_CYAN(s): return s
    def TXT_GRAY(s): return s

# error handling
def warn(msg: any):
    print(TXT_YELLOW(f'[WARN] {msg}'), flush=True, file=sys.stderr)

def error(msg: any):
    print(TXT_RED(f'[ERR!] {msg}'), file=sys.stderr)
    sys.exit(1)

# build option parsing
class GNUStyleHelpFormatter(argparse.RawDescriptionHelpFormatter):
    """Custom help formatter that forces --opt=VALUE style"""
    def _format_action_invocation(self, action):
        # only long options, no short option
        if not action.option_strings:
            return super()._format_action_invocation(action)
        return ', '.join(f'{opt}={action.metavar}' if action.metavar else opt
                         for opt in action.option_strings)

def parse_args() -> tuple[argparse.Namespace, list[str]]:
    epilog = '''Some influential environment variables:
    CC                 C compiler command
    CFLAGS             Extra C compiler flags
    LDFLAGS            Extra linker flags
    ASCIIDOC           Asciidoc document compiler

Please report bug at https://github.com/dbgbgtf1/Ceccomp
Copyright (C) 2025-present, distributed under GPLv3 or later
    '''
    parser = argparse.ArgumentParser(
        prog='configure.py',
        description='Configure build flags for Ceccomp, written in Python',
        epilog=epilog,
        formatter_class=GNUStyleHelpFormatter,
    )

    config_grp = parser.add_argument_group('Configure-time options (fixed once Makefile is generated)')
    config_grp.add_argument('--enable-verbose', action='store_true', help='Enable verbose output')
    config_grp.add_argument('--without-doc', action='store_true', help='Do not install documentation')
    config_grp.add_argument('--without-i18n', action='store_true', help='Do not generate locale mo files')
    config_grp.add_argument('--devmode', action='store_true', help='Allow you to compile against any commit (only work in git mode)')
    config_grp.add_argument('--packager', type=str, metavar='BY', default='manual', help='Who build the package (default: manual)')
    config_grp.add_argument('--disable-option-checking', action='store_true', help='Whether to throw an error when unknown options found')
    config_grp.add_argument('--build', type=str, metavar='TARGET',
        help='Which target to build against, optional. For instance, x86_64-linux-gnu')
    config_grp.add_argument('--includedir', type=str, metavar='DIR', help='Optional system include directory')
    config_grp.add_argument('--libdir', type=str, metavar='DIR', help='Optional system library directory')
    # argp is dynamically tested

    make_grp = parser.add_argument_group('Make-time options (can be overridden by make cli) [make var]')
    make_grp.add_argument('--prefix', type=str, metavar='PREFIX', default='/usr',
        help='Installation prefix (default: /usr) [PREFIX]')
    make_grp.add_argument('--bindir', type=str, metavar='DIR',
        help='User executables directory (default: PREFIX/bin) [BIN_DIR]')
    make_grp.add_argument('--zshfpath', type=str, metavar='DIR',
        help='Zsh functions path (default: PREFIX/share/zsh/site-functions) [ZSH_FPATH]')
    make_grp.add_argument('--docdir', type=str, metavar='DIR',
        help='Documentation directory (default: PREFIX/share/doc/ceccomp) [DOC_DIR]')
    make_grp.add_argument('--mandir', type=str, metavar='DIR',
        help='Man pages directory (default: PREFIX/share/man) [MAN_DIR]')
    make_grp.add_argument('--localedir', type=str, metavar='DIR',
        help='Localization directory (default: PREFIX/share/locale) [LOCALE_DIR]')
    make_grp.add_argument('--destdir', type=str, metavar='DIR',
        help='Optional dir to install software, useful for package managers [DESTDIR]')
    make_grp.add_argument('--debug-level', type=int, metavar='LEVEL', default=1,
        help='Set debug symbol level (0: -O2 -s|1: -O2 -g, default|2: -O0 -g3 -DDEBUG) [DEBUG]')
    make_grp.add_argument('--version', type=str, metavar='VER',
        help=f'Set ceccomp version (default: {VERSION}) [VERSION]')
    make_grp.add_argument('--tag-time', type=str, metavar='TIME',
        help=f'Set the time of current tag. Use "CONFIG" for the time when running configure; or write in YYYY-mm-dd like default value {TAG_TIME} [TAG_TIME]')
    make_grp.add_argument('--enable-static', action='store_true', help='Enable static build [STATIC]')

    return parser.parse_known_args()

@dataclass
class BuildOptions:
    prefix: str
    bindir: str
    zshfpath: str
    docdir: str
    mandir: str
    localedir: str
    destdir: str
    debug_level: int
    version: str
    tag_time: str
    is_static: bool

    verbose: bool
    doc: bool
    i18n: bool
    devmode: bool
    builder: str
    check_option: bool
    target: str
    sys_includedir: str
    sys_libdir: str

    cc: str | None
    asciidoc: str | None
    cflags: str
    ldflags: str

    unknown_options: list[str]

    git_install: bool = True

    def __post_init__(self):
        if self.bindir is None:
            self.bindir = os.path.join(self.prefix, 'bin')
        if self.zshfpath is None:
            self.zshfpath = os.path.join(self.prefix, 'share', 'zsh', 'site-functions')
        if self.docdir is None:
            self.docdir = os.path.join(self.prefix, 'share', 'doc', 'ceccomp')
        if self.mandir is None:
            self.mandir = os.path.join(self.prefix, 'share', 'man')
        if self.localedir is None:
            self.localedir = os.path.join(self.prefix, 'share', 'locale')

        def replace_prefix(dirent: str | None) -> str:
            return dirent.replace('${prefix}', self.prefix) if dirent else ''

        self.bindir = replace_prefix(self.bindir)
        self.zshfpath = replace_prefix(self.zshfpath)
        self.docdir = replace_prefix(self.docdir)
        self.mandir = replace_prefix(self.mandir)
        self.localedir = replace_prefix(self.localedir)
        self.destdir = replace_prefix(self.destdir)

        self.sys_includedir = replace_prefix(self.sys_includedir)
        self.sys_libdir = replace_prefix(self.sys_libdir)

        if self.debug_level not in (0, 1, 2):
            error('debug_level option can only be 0, 1 or 2!')

        def check_cmd(env: str, *candidates: list[str]) -> str | None:
            """enumerate available command in system"""
            var = getattr(self, env)
            if var is not None:
                if shutil.which(var) is None:
                    error(f'{var} provided by environment {env.upper()} is not runnable!')
                else:
                    return var
            # no such environment variable
            for candidate in candidates:
                if shutil.which(candidate) is not None:
                    return candidate
            return None

        # the None value will be processed in check
        if self.target:
            self.cc = check_cmd('cc', f'{self.target}-cc', f'{self.target}-gcc', f'{self.target}-clang')
            if not self.cc:
                warn(f'No C compiler with prefix {self.target} found')
                self.cc = check_cmd('cc', 'cc', 'gcc', 'clang')
        else: # cc may be passed from environ, so we shall not merge the logic
            self.cc = check_cmd('cc', 'cc', 'gcc', 'clang')
        self.asciidoc = check_cmd('asciidoc', 'asciidoctor')
        if self.cflags is None:
            self.cflags = ''
        if self.ldflags is None:
            self.ldflags = ''
        if self.tag_time == 'CONFIG':
            self.tag_time = datetime.datetime.today().strftime('%Y-%m-%d')
        elif self.tag_time:
            try:
                datetime.datetime.strptime(self.tag_time, '%Y-%m-%d')
            except ValueError:
                error(f'Invalid tag-time {self.tag_time}, format is YYYY-mm-dd')

        # check the options
        if self.unknown_options:
            if self.check_option:
                error(f'Unrecognized options: {" ".join(self.unknown_options)}, please pass --help to see correct options!')
            else:
                for opt in self.unknown_options:
                    print(TXT_GRAY(f'Ignoring unknown option {opt}'))
                warn(f'Discarding {len(self.unknown_options)} unknown options!')

args, unknown = parse_args()
buildopts = BuildOptions(
    prefix=args.prefix,
    bindir=args.bindir,
    zshfpath=args.zshfpath,
    docdir=args.docdir,
    mandir=args.mandir,
    localedir=args.localedir,
    destdir=args.destdir,
    debug_level=args.debug_level,
    version=args.version,
    tag_time=args.tag_time,
    is_static=args.enable_static,

    verbose=args.enable_verbose,
    doc=not args.without_doc,
    i18n=not args.without_i18n,
    devmode=args.devmode,
    builder=args.packager,
    check_option=not args.disable_option_checking,
    target=args.build,
    sys_includedir=args.includedir,
    sys_libdir=args.libdir,

    cc=os.getenv('CC'),
    asciidoc=os.getenv('ASCIIDOC'),
    cflags=os.getenv('CFLAGS'),
    ldflags=os.getenv('LDFLAGS'),

    unknown_options=unknown,
)
del args, unknown

# gathering system information and check
@dataclass
class Makefile:
    s: str
    def inject(self, placeholder: str, filler: str | list[str] | bool) -> None:
        if isinstance(filler, str):
            self.s = self.s.replace(f'{{{{{placeholder}}}}}', filler) # {{PLACEHOLDER}}
            return
        if isinstance(filler, bool):
            # automatically loop from 0 to perform substitution
            keep = filler
            i = 0
            while True:
                if self.s.find(f'{{{{{placeholder}_IF}}}}') == -1:
                    break
                if keep:
                    self.s = self.s.replace(f'{{{{{placeholder}_IF}}}}', '') # {{PLACEHOLDER_IF}}
                    self.s = self.s.replace(f'{{{{{placeholder}_ENDIF}}}}', '')
                else:
                    begin = self.s.find(f'{{{{{placeholder}_IF}}}}')
                    end   = self.s.find(f'{{{{{placeholder}_ENDIF}}}}')
                    assert begin != -1
                    assert end   != -1
                    end += len(f'{{{{{placeholder}_ENDIF}}}}')
                    self.s = self.s[:begin] + self.s[end:]
                i += 1
            return
        for i, e in enumerate(filler):
            self.s = self.s.replace(f'{{{{{placeholder}{i}}}}}', e) # {{PLACEHOLDER0}}

class TaskManager:
    tasks: list[Callable[[Makefile], None]]
    makefile: Makefile

    def __init__(self, makefile_name: str) -> None:
        self.tasks = []
        if not os.path.isfile(makefile_name):
            error(f'Reading Makefile {makefile_name}, but it\'s not a file!')
        try:
            with open(makefile_name) as file:
                self.makefile = Makefile(file.read())
        except OSError as e:
            error(f'Can not open Makefile: {e}')

    def run_tasks(self):
        width = len(str(len(self.tasks)))
        total = len(self.tasks)
        for seq, handler in enumerate(self.tasks, start=1):
            # no need to flush as stdout will be flushed in handler
            print(f'[{seq:>{width}}/{total}] ', end='')
            handler(self.makefile)

    def add_task(self, handler: Callable[[Makefile], None]):
        self.tasks.append(handler)

taskmgr = TaskManager('Makefile.in')


def pcheck(txt: str):
    print(f'Checking {txt}... ', end='', flush=True)

def run_command(argv: list[str], stdin: str | None=None, push_sys_dir: bool=False) -> tuple[int, str, str]:
    """
    Run command with given argv and input, return process returncode, stdout and stderr
    Return -1, '', exception message if exception raised is known, or else return -2, '', message.
    """
    try:
        argv[0] = shutil.which(argv[0])
        if push_sys_dir: # using C compiler!
            if buildopts.sys_includedir: # in case some include/lib is not accessible for current compiler
                argv.append(f'-I{buildopts.sys_includedir}')
            if buildopts.sys_libdir:
                argv.append(f'-L{buildopts.sys_libdir}')
            if buildopts.is_static:
                argv.append('-static')
        proc = subprocess.run(argv, input=stdin, capture_output=True, text=True)
    except subprocess.SubprocessError as e:
        return -1, '', str(e)
    except UnicodeDecodeError as e:
        return -1, '', 'UnicodeDecodeError: program output contains non-utf-8 bytes'
    except Exception as e:
        return -2, '', str(e)
    else:
        return proc.returncode, proc.stdout, proc.stderr

def new_task(func: Callable) -> Callable:
    taskmgr.add_task(func)
    return func


# color definition:
# green:   well-tested
# cyan:    should work
# blue:    not work and have to take fallback choice
# magenta: unexpected or no fallback choice
@new_task
def check_platform(_: str):
    pcheck('system platform')
    if sys.platform == 'linux':
        print(TXT_GREEN('linux'))
    elif sys.platform == 'android':
        print(TXT_CYAN('android'))
    else:
        print(TXT_MAGENTA(sys.platform))
        error(f'Ceccomp only support Linux!')

@new_task
def check_flock(makefile: Makefile):
    pcheck('if flock in system')
    if shutil.which('flock') is None:
        makefile.inject('VERBOSE', '1')
        print(TXT_BLUE('no'))
        warn('flock not found in system, Makefile verbose is set to true. You may need util-linux package')
    else:
        makefile.inject('VERBOSE', '1' if buildopts.verbose else '0')
        print(TXT_GREEN('yes'))

@new_task
def check_i18n(makefile: Makefile):
    pcheck('internationalization support')
    if not buildopts.i18n:
        makefile.inject('I18N', False)
        makefile.inject('LOCALEDIR', buildopts.localedir)
        makefile.inject('LOCALE_SED', 's|@@LOCALEDIR@@||')
        print(TXT_BLUE('no'))
        return
    if shutil.which('xgettext') is None:
        print(TXT_MAGENTA('no gettext'))
        error('xgettext and other tools not found, pass --without-i18n to disable l10n generation or install gettext package!')
    makefile.inject('I18N', True)
    makefile.inject('LOCALEDIR', buildopts.localedir)
    makefile.inject('LOCALE_SED', f's|@@LOCALEDIR@@|#define LOCALEDIR "{buildopts.localedir}"|')
    print(TXT_GREEN('yes'))

@new_task
def check_doc(makefile: Makefile):
    pcheck('documentation settings')
    if not buildopts.doc:
        makefile.inject('DOC', False)
        print(TXT_BLUE('no doc'))
        return
    if buildopts.asciidoc is None:
        print(TXT_MAGENTA('no asciidoc found'))
        error('Asciidoc compiler not found, pass --without-doc to disable documentation generation or install asciidoctor package!')
    makefile.inject('DOC', True)
    makefile.inject('ASCIIDOC', buildopts.asciidoc)
    if 'asciidoctor' in buildopts.asciidoc:
        print(TXT_GREEN(buildopts.asciidoc))
    else:
        print(TXT_CYAN(buildopts.asciidoc))
        warn(f'Your asciidoc compiler {buildopts.asciidoc} is not tested and may fail to generate docs')

standard_c = '''
#include <stdio.h>
void leaf(void) {}
int main() {
    puts("");
    leaf();
    return 0;
}
'''
@new_task
def check_cc(makefile: Makefile):
    pcheck('C compiler')
    if buildopts.cc is None:
        print(TXT_MAGENTA('no cc found'))
        error('C compiler command not found in system!')
    code, _, stderr = run_command([buildopts.cc, '-x', 'c', '-', '-o', '/dev/null'], standard_c)
    if code:
        print(TXT_MAGENTA(buildopts.cc))
        if code == -1:
            error(f'C compiler is not working: {stderr}')
        if code == -2:
            error(f'Unexpected error: {stderr}')
        print(f'Compiler returned non-zero returncode with message:\n{stderr}', end='')
        error(f'C compiler can not compile minimum C unit')
    # code == 0
    makefile.inject('CC', buildopts.cc)
    print(TXT_GREEN(buildopts.cc))


linux_headers = '''
#include <unistd.h>
#include <linux/filter.h>
#include <sys/mman.h>
int main() {
    struct sock_fprog *addr = (struct sock_fprog *)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
    *addr = (struct sock_fprog){ .len = 0, .filter = NULL };
    return 0;
}
'''
@new_task
def check_linux_headers(_: Makefile):
    pcheck('Linux-related headers')
    code, _, stderr = run_command([buildopts.cc, '-x', 'c', '-', '-o', '/dev/null'], linux_headers, push_sys_dir=True)
    if code:
        print(TXT_MAGENTA('no'))
        if code == -1:
            error(f'C compiler is not working: {stderr}')
        if code == -2:
            error(f'Unexpected error: {stderr}')
        print(f'Compiler returned non-zero returncode with message:\n{stderr}', end='')
        error(f'Can not compile Linux-related unit, you may install linux-headers package')
    print(TXT_GREEN('yes'))


libseccomp = '''
#include <stdio.h>
#include <seccomp.h>
int main() {
    printf("%d", seccomp_version()->major);
    return 0;
}
'''
@new_task
def check_libseccomp(_: Makefile):
    pcheck('libseccomp')
    code, _, stderr = run_command([buildopts.cc, '-x', 'c', '-', '-lseccomp', '-o', '/dev/null'], libseccomp, push_sys_dir=True)
    if code:
        print(TXT_MAGENTA('no'))
        if code == -1:
            error(f'C compiler is not working: {stderr}')
        if code == -2:
            error(f'Unexpected error: {stderr}')
        print(f'Compiler returned non-zero returncode with message:\n{stderr}', end='')
        error(f'Can not compile libseccomp unit, you may install libseccomp package')
    print(TXT_GREEN('yes'))


argp = '''
#include <argp.h>
int main(int argc, char **argv) {
    return argp_parse(0, argc, argv, 0, 0, 0);
}
'''
@new_task
def check_argp(makefile: Makefile):
    pcheck('argp parser')
    code, _, stderr = run_command([buildopts.cc, '-x', 'c', '-', '-o', '/dev/null'], argp, push_sys_dir=True)
    if code:
        if code == -1:
            print(TXT_MAGENTA('no'))
            error(f'C compiler is not working: {stderr}')
        if code == -2:
            print(TXT_MAGENTA('no'))
            error(f'Unexpected error: {stderr}')
        # code > 0
        # some system need external argp package, so we try to link
        code, _, stderr = run_command([buildopts.cc, '-x', 'c', '-', '-largp', '-o', '/dev/null'], argp)
        if code:
            print(TXT_MAGENTA('no'))
            if code == -1:
                print(TXT_MAGENTA('no'))
                error(f'C compiler is not working: {stderr}')
            if code == -2:
                print(TXT_MAGENTA('no'))
                error(f'Unexpected error: {stderr}')
            print(f'Compiler returned non-zero returncode with message:\n{stderr}', end='')
            error(f'Can not compile argp unit, you may install argp package')
        # code == 0 and link argp
        makefile.inject('LIBARGP', '-largp')
        print(TXT_CYAN('external'))
        return
    # code == 0
    makefile.inject('LIBARGP', '')
    print(TXT_GREEN('builtin'))

@new_task
def check_source(_: Makefile):
    pcheck('source code type')
    if os.path.isdir('.git'):
        buildopts.git_install = True
        print(TXT_GREEN('git install'))
    else:
        buildopts.git_install = False
        print(TXT_CYAN('tarball install'))

@new_task
def check_version(makefile: Makefile):
    pcheck('target version')
    if buildopts.git_install:
        if not buildopts.devmode:
            code, stdout, stderr = run_command(['git', 'describe', '--exact-match'])
            if code < 0:
                print(TXT_MAGENTA('git failed'))
                print(f'Git not accessible? {stderr}')
                error('Can not run git, you may install git package')

            elif code == 0: # we are at a tag, adopt it
                tag = stdout.strip()
                buildopts.version = tag if not tag.startswith('v') else tag[1:]
                code, stdout, _ = run_command(['git', 'log', '-1', '--format=format:%as', tag])
                if code:
                    print(TXT_MAGENTA('get tag time failed'))
                    print(stderr, end='' if stderr[-1] == '\n' else '\n')
                    error('Can not get tag-time for the latest tag')
                buildopts.tag_time = stdout.strip()

            else: # we are not at a tag, try to go to a tag
                code, stdout, _ = run_command(['git', 'rev-list', '--tags', '--max-count=1'])
                if code:
                    print(TXT_MAGENTA('git failed'))
                    print(stderr, end='' if stderr[-1] == '\n' else '\n')
                    error('Can not determine the latest tag by git, to build from git, you need to pull the whole repo!')
                code, stdout, _ = run_command(['git', 'describe', '--tags', stdout.strip()])
                if code:
                    print(TXT_MAGENTA('git failed'))
                    print(stderr, end='' if stderr[-1] == '\n' else '\n')
                    error('Can not determine the latest tag by git')
                tag = stdout.strip()
                code, stdout, _ = run_command(['git', 'checkout', tag]) # checkout to that tag
                if code:
                    print(TXT_MAGENTA('checkout failed'))
                    print(stderr, end='' if stderr[-1] == '\n' else '\n')
                    error('Can not checkout the latest tag')
                print(TXT_CYAN(tag))
                warn('Checked out to new tag, please rerun me to build stable version of ceccomp!')
                sys.exit(0)

        else: # devmode is True
            code, stdout, stderr = run_command(['git', 'describe', '--long'])
            if code < 0:
                print(TXT_MAGENTA('git failed'))
                print(f'Git not accessible? {stderr}')
                error('Can not run git, you may install git package')

            elif code == 0: # current commit get correctly resolved (tag-rev-gcommit or tag)
                # --long mode display in TAG-COMMITS-gCOMMIT
                tag, revision, commit = stdout.strip().split('-')
                ver = tag if not tag.startswith('v') else tag[1:]
                buildopts.version = f'{ver}.r{revision}_{commit[1:]}' # skip g
                code, stdout, _ = run_command(['git', 'log', '-1', '--format=format:%as'])
                if code:
                    print(TXT_MAGENTA('get tag time failed'))
                    print(stderr, end='' if stderr[-1] == '\n' else '\n')
                    error('Can not get tag-time for the latest tag')
                buildopts.tag_time = stdout.strip()

            else: # git failed to describe
                print(TXT_MAGENTA('describe failed'))
                print(stderr, end='' if stderr[-1] == '\n' else '\n')
                error('Can not determine the latest tag by git, to build from git, you need to pull the whole repo!')

    else: # tarball install
        if not buildopts.version:
            buildopts.version = VERSION
        if not buildopts.tag_time:
            buildopts.tag_time = TAG_TIME

    makefile.inject('VERSION', buildopts.version)
    makefile.inject('TAG_TIME', buildopts.tag_time)
    if buildopts.git_install and not buildopts.devmode:
        print(TXT_GREEN(buildopts.version))
    elif buildopts.git_install and buildopts.devmode:
        print(TXT_BLUE(buildopts.version))
    else:
        print(TXT_CYAN(buildopts.version))

@new_task
def check_builder_name(makefile: Makefile):
    pcheck('if builder name is valid')
    if '/' in buildopts.builder:
        error(f'Found \'/\' in builder name {buildopts.builder}, which is not allowed.')
    makefile.inject('BUILDER', buildopts.builder)
    print(TXT_GREEN('yes'))

@new_task
def check_omit_leaf_frame_pointer_flag(makefile: Makefile):
    pcheck('if compiler support -mno-omit-leaf-frame-pointer')
    code, _, _ = run_command([buildopts.cc, '-x', 'c', '-Werror', '-',
                                   '-mno-omit-leaf-frame-pointer', '-o', '/dev/null'],
                                   standard_c, push_sys_dir=True)
    if code:
        print(TXT_CYAN('no'))
        makefile.inject('ARCH_PRESERVE_FP', '')
    else:
        print(TXT_GREEN('yes'))
        makefile.inject('ARCH_PRESERVE_FP', '-mno-omit-leaf-frame-pointer')

taskmgr.run_tasks()
# vars not injected: EXTRA_CFLAGS EXTRA_LDFLAGS DEBUG_LEVEL PREFIX BINDIR ZSH_FPATH MANDIR DOCDIR DESTDIR SYS_INC_DIR SYS_LIB_DIR IS_STATIC
taskmgr.makefile.inject('EXTRA_CFLAGS', buildopts.cflags)
taskmgr.makefile.inject('EXTRA_LDFLAGS', buildopts.ldflags)
taskmgr.makefile.inject('DEBUG_LEVEL', str(buildopts.debug_level))
taskmgr.makefile.inject('PREFIX', buildopts.prefix)
taskmgr.makefile.inject('BINDIR', buildopts.bindir)
taskmgr.makefile.inject('ZSH_FPATH', buildopts.zshfpath)
taskmgr.makefile.inject('MANDIR', buildopts.mandir)
taskmgr.makefile.inject('DOCDIR', buildopts.docdir)
taskmgr.makefile.inject('DESTDIR', buildopts.destdir)
taskmgr.makefile.inject('IS_STATIC', '1' if buildopts.is_static else '0')

taskmgr.makefile.inject('SYS_INC_DIR', f'-I{buildopts.sys_includedir}' if buildopts.sys_includedir else '')
taskmgr.makefile.inject('SYS_LIB_DIR', f'-L{buildopts.sys_libdir}' if buildopts.sys_libdir else '')

assert taskmgr.makefile.s.find('{{') == -1

print('Writting back to Makefile... ', end='', flush=True)
try:
    with open('Makefile', 'w') as f:
        f.write(taskmgr.makefile.s)
except Exception as e:
    print(TXT_RED('failed'))
    error(f'Unexpected error when write back: {e}')
else:
    print(TXT_GREEN('ok'))
