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

# constants
VERSION = '4.3'
TAG_TIME = '2026-9-7'

# 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'
    def TXT_BRIGHT_BLUE(s): return f'\x1b[94m{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
    def TXT_BRIGHT_BLUE(s): return s

# error handling
def info(*args, **kwargs):
    print(*args, **kwargs, flush=True)

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
    BPF_CC             BPF compiler (specify clang or bpf-*-gcc)
    BPFTOOL            BPF tool command
    BPF_CFLAGS         Extra BPF C compiler flags

Please report bug at https://github.com/ceccomp/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('--without-ebpf', action='store_true', help='Disable ebpf support')
    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=None,
                            help='Who build the package (default: manual or OS name if setting prefix to /usr)')
    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')
    config_grp.add_argument('--vmlinuxdir', type=str, metavar='DIR', default='auto',
                            help='The directory containing vmlinux or vmlinux.h for eBPF support. '
                                 '"auto" (detect vmlinux.h or vmlinux automatically) and '
                                 '"generated" (always generate vmlinux.h from vmlinux in default /sys path) '
                                 'are special values configure accepts.')
    # 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/local',
        help='Installation prefix (default: /usr/local) [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()

is_gcc_bpf: bool

@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 | None
    check_option: bool
    target: str
    sys_includedir: str
    sys_libdir: str
    vmlinuxdir: str

    cc: list[str] | None
    asciidoc: str | None
    cflags: str
    ldflags: str
    bpf_cflags: str

    unknown_options: list[str]

    ebpf: bool
    bpf_cc: list[str] | None
    bpftool: str
    bpf_arch: str | None

    has_set_packager: bool = False
    is_official_package: bool = False
    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)
        self.vmlinuxdir = replace_prefix(self.vmlinuxdir)

        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 | list[str] | None:
            """enumerate available command in system"""
            var = getattr(self, env)
            if var is not None:
                if isinstance(var, str) and shutil.which(var) is None:
                    error(f'{var} provided by environment {env.upper()} is not runnable!')
                elif isinstance(var, list) and shutil.which(var[0]) is None:
                    error(f'{var[0]} 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

        self.cc = self.cc.strip().split() if self.cc else None
        if isinstance(self.cc, list) and len(self.cc) == 1:
            self.cc = self.cc[0]

        # 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, append it in CC')
                self.cc = check_cmd('cc', 'clang', 'gcc', 'cc')
            # clang and zig cc adds target to back
            if not (isinstance(self.cc, str) and self.target in self.cc):
                if isinstance(self.cc, str):
                    self.cc = [self.cc, '-target', self.target]
                elif isinstance(self.cc, list): # could be None
                    self.cc.extend(['-target', self.target])
        else: # cc may be passed from environ, so we shall not merge the logic
            self.cc = check_cmd('cc', 'cc', 'gcc', 'clang')
        if isinstance(self.cc, str):
            self.cc = [self.cc]
        self.asciidoc = check_cmd('asciidoc', 'asciidoctor')
        if self.cflags is None:
            self.cflags = ''
        if self.ldflags is None:
            self.ldflags = ''
        if self.bpf_cflags is None:
            self.bpf_cflags = ''
        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:
                    info(TXT_GRAY(f'Ignoring unknown option {opt}'))
                warn(f'Discarding {len(self.unknown_options)} unknown options!')

        self.is_official_package = self.prefix == '/usr'
        self.has_set_packager = self.builder is not None
        try:
            if not self.has_set_packager:
                if self.is_official_package:
                    self.builder = platform.freedesktop_os_release()['NAME']
                else:
                    self.builder = 'manual'
        except (OSError, KeyError):
            self.builder = 'manual'

        # ebpf section
        self.bpftool = check_cmd('bpftool', 'bpftool')

        self.bpf_cc = self.bpf_cc.strip().split() if self.bpf_cc else None
        bpf_cc = check_cmd('bpf_cc', 'clang', 'bpf-gcc')
        if isinstance(bpf_cc, str):
            bpf_cc = [bpf_cc]

        global is_gcc_bpf
        is_gcc_bpf = False
        if bpf_cc:
            if 'bpf' in bpf_cc[0]:
                bpf_cc.append('-mlittle-endian')
                is_gcc_bpf = True
            else:
                bpf_cc.append('-target')
                bpf_cc.append('bpf')
            self.bpf_cc = bpf_cc


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,
    vmlinuxdir=args.vmlinuxdir,

    ebpf=not args.without_ebpf,
    bpf_cc=os.getenv('BPF_CC'),
    bpftool=os.getenv('BPFTOOL'),
    bpf_arch=None,

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

    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
            info(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):
    info(f'Checking {txt}... ', end='')

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 return code, 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

pkg_config = shutil.which('pkg-config')
def probe_library_flags(lib: str) -> list[str]:
    """
    Retrieve compiler link flag via pkg-config, or simply `-llib`. lib is library name
    without lib prefix like seccomp.
    """
    if not pkg_config:
        return [f'-l{lib}']
    argv = [pkg_config, '--libs', f'lib{lib}']
    if buildopts.is_static:
        argv.append('--static')
    code, flags, _ = run_command(argv)
    if code:
        return [f'-l{lib}']
    return flags.strip().split()

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
# bright blue: not work but silently disabled
@new_task
def check_platform(_: Makefile):
    pcheck('system platform')
    if sys.platform == 'linux':
        info(TXT_GREEN('linux'))
    elif sys.platform == 'android':
        info(TXT_CYAN('android'))
    else:
        info(TXT_MAGENTA(sys.platform))
        error(f'Ceccomp only support Linux!')

TIER_1_ARCH = [ # tested
    'x86_64', 'i386', 'i686', 'riscv64', 'loongarch64', 'aarch64',
    'ppc', 'ppc64le', 's390x', 'armv8l', 'armv7l',
]
TIER_2_ARCH = [ # untested, but listed in libseccomp
    'x32', 'parisc', 'parisc64', 'mips', 'm68k', 's390', 'ppc64', 'arm',
    'sh', 'sh4', 'shel',
]
@new_task
def check_architecture(_: Makefile):
    pcheck('current architecture')
    bits = platform.architecture()[0]
    arch = platform.machine()
    mach = f'{bits}, {arch}'
    if arch in TIER_1_ARCH:
        info(TXT_GREEN(mach))
    elif arch in TIER_2_ARCH:
        info(TXT_CYAN(mach))
    else:
        info(TXT_BLUE(mach))
        warn('Current architecture is not supported by libseccomp')

@new_task
def check_flock(makefile: Makefile):
    pcheck('if flock in system')
    if shutil.which('flock') is None:
        makefile.inject('VERBOSE', '1')
        info(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')
        info(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@@||')
        info(TXT_BLUE('excluded'))
        return
    if shutil.which('xgettext') is None:
        makefile.inject('I18N', False)
        makefile.inject('LOCALEDIR', buildopts.localedir)
        makefile.inject('LOCALE_SED', 's|@@LOCALEDIR@@||')
        info(TXT_BRIGHT_BLUE('no'))
        warn('xgettext and other tools not found, install gettext package to enable l10n!')
        return

    makefile.inject('I18N', True)
    makefile.inject('LOCALEDIR', buildopts.localedir)
    makefile.inject('LOCALE_SED', f's|@@LOCALEDIR@@|#define LOCALEDIR "{buildopts.localedir}"|')
    info(TXT_GREEN('yes'))

@new_task
def check_doc(makefile: Makefile):
    pcheck('documentation generator')
    if not buildopts.doc:
        makefile.inject('DOC', False)
        info(TXT_BLUE('excluded'))
        return
    if buildopts.asciidoc is None:
        makefile.inject('DOC', False)
        info(TXT_BRIGHT_BLUE('no'))
        warn('Asciidoc compiler not found, install asciidoctor package for man page!')
        return
    makefile.inject('DOC', True)
    makefile.inject('ASCIIDOC', buildopts.asciidoc)
    if 'asciidoctor' in buildopts.asciidoc:
        info(TXT_GREEN(buildopts.asciidoc))
    else:
        info(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:
        info(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:
        info(TXT_MAGENTA(' '.join(buildopts.cc)))
        if code == -1:
            error(f'C compiler is not working: {stderr}')
        if code == -2:
            error(f'Unexpected error: {stderr}')
        info(f'Compiler returned non-zero return code with message:\n{stderr}', end='')
        error(f'C compiler can not compile minimum C unit')
    # code == 0
    makefile.inject('CC', ' '.join(buildopts.cc))
    info(TXT_GREEN(' '.join(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:
        info(TXT_MAGENTA('no'))
        if code == -1:
            error(f'C compiler is not working: {stderr}')
        if code == -2:
            error(f'Unexpected error: {stderr}')
        info(f'Compiler returned non-zero return code with message:\n{stderr}', end='')
        error(f'Can not compile Linux-related unit, you may install linux-headers package')
    info(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: Makefile):
    pcheck('libseccomp')
    link_flags = probe_library_flags('seccomp')
    code, _, stderr = run_command([*buildopts.cc, '-x', 'c', '-',
                                   *link_flags, '-o', '/dev/null'],
                                  libseccomp, push_sys_dir=True)
    if code:
        info(TXT_MAGENTA('no'))
        if code == -1:
            error(f'C compiler is not working: {stderr}')
        if code == -2:
            error(f'Unexpected error: {stderr}')
        info(f'Compiler returned non-zero return code with message:\n{stderr}', end='')
        error(f'Can not compile libseccomp unit, you may install libseccomp package')
    info(TXT_GREEN('yes'))
    makefile.inject('LIBSECCOMP_FLAG', ' '.join(link_flags))


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:
            info(TXT_MAGENTA('no'))
            error(f'C compiler is not working: {stderr}')
        if code == -2:
            info(TXT_MAGENTA('no'))
            error(f'Unexpected error: {stderr}')
        # code > 0
        # some system need external argp package, so we try to link
        argp_flags = probe_library_flags('argp')
        code, _, stderr = run_command([*buildopts.cc, '-x', 'c', '-',
                                       *argp_flags, '-o', '/dev/null'],
                                      argp, push_sys_dir=True)
        if code:
            info(TXT_MAGENTA('no'))
            if code == -1:
                info(TXT_MAGENTA('no'))
                error(f'C compiler is not working: {stderr}')
            if code == -2:
                info(TXT_MAGENTA('no'))
                error(f'Unexpected error: {stderr}')
            info(f'Compiler returned non-zero return code 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_FLAG', ' '.join(argp_flags))
        info(TXT_CYAN('external'))
        return
    # code == 0
    makefile.inject('LIBARGP_FLAG', '')
    info(TXT_GREEN('builtin'))

@new_task
def check_source(_: Makefile):
    pcheck('source code type')
    if os.path.isdir('.git'):
        buildopts.git_install = True
        info(TXT_GREEN('git install'))
    else:
        buildopts.git_install = False
        info(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:
                info(TXT_MAGENTA('git failed'))
                info(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:
                    info(TXT_MAGENTA('get tag time failed'))
                    info(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:
                    info(TXT_MAGENTA('git failed'))
                    info(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:
                    info(TXT_MAGENTA('git failed'))
                    info(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:
                    info(TXT_MAGENTA('checkout failed'))
                    info(stderr, end='' if stderr[-1] == '\n' else '\n')
                    error('Could not checkout the latest tag. Do you need to `git stash` changes?')
                info(TXT_CYAN(tag))
                warn('Checked out to new tag, please rerun me to build stable version of ceccomp! '
                     '(or use --devmode to build on an arbitrary commit)')
                sys.exit(0)

        else: # devmode is True
            code, stdout, stderr = run_command(['git', 'describe', '--long'])
            if code < 0:
                info(TXT_MAGENTA('git failed'))
                info(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:
                    info(TXT_MAGENTA('get tag time failed'))
                    info(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
                info(TXT_MAGENTA('describe failed'))
                info(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:
        info(TXT_GREEN(buildopts.version))
    elif buildopts.git_install and buildopts.devmode:
        info(TXT_BLUE(buildopts.version))
    else:
        info(TXT_CYAN(buildopts.version))

@new_task
def check_builder_name(makefile: Makefile):
    pcheck('builder name')
    if '^' in buildopts.builder:
        info(TXT_MAGENTA(buildopts.builder))
        error(f"Found '^' in builder name {buildopts.builder}, which is not allowed.")
    if not buildopts.builder.strip():
        info(TXT_MAGENTA('(empty)'))
        error(f'Empty builder name is not accepted.')
    makefile.inject('BUILDER', buildopts.builder)
    if buildopts.has_set_packager:
        info(TXT_CYAN(buildopts.builder))
    elif buildopts.is_official_package and buildopts.builder != 'manual':
        info(TXT_GREEN(buildopts.builder))
    else:
        info(TXT_BLUE(buildopts.builder))

@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:
        info(TXT_CYAN('no'))
        makefile.inject('ARCH_PRESERVE_FP', '')
    else:
        info(TXT_GREEN('yes'))
        makefile.inject('ARCH_PRESERVE_FP', '-mno-omit-leaf-frame-pointer')

found_llvm_strip = False

@new_task
def check_ebpf_infra(_: Makefile):
    pcheck('eBPF support tools')
    if not buildopts.ebpf:
        info(TXT_BLUE('excluded'))
        return
    if not buildopts.bpftool:
        info(TXT_BRIGHT_BLUE('no'))
        warn('Can not find bpftool, you may install it to enable eBPF support')
        buildopts.ebpf = False
        return
    if not buildopts.bpf_cc:
        info(TXT_BRIGHT_BLUE('no'))
        warn('Can not find compiler for BPF, you may install clang to enable eBPF support')
        buildopts.ebpf = False
        return

    info(TXT_GREEN(f'yes, {buildopts.bpf_cc[0]}'))

    if shutil.which('llvm-strip'):
        global found_llvm_strip
        found_llvm_strip = True
    elif not is_gcc_bpf:
        warn('Can not find llvm-strip to strip bpf object, the final binary may be large')

tmpdir = None
vmlinux_h_flags = []
ebpf_source = '''
#include <vmlinux.h>
struct seccomp_filter *test(struct task_struct *task) {
    return task->seccomp.filter;
}
'''
@new_task
def check_vmlinuxdir(makefile: Makefile):
    pcheck('vmlinux.h location')
    if not buildopts.ebpf:
        info(TXT_BLUE('skipped'))
        return

    def _test_bpfcc(vmlinux_h_dir: str, suppress_error: bool=False,
                    tmp: tempfile.TemporaryDirectory | None=None) -> bool:
        # no return if error, or when suppress_error enabled, False is returned
        bpfcc_flags = []
        if is_gcc_bpf:
            # bpf-gcc
            bpfcc_flags = ['-O2', '-std=gnu17']
        code, _, stderr = run_command([*buildopts.bpf_cc, '-x', 'c', '-',
                                       '-o', '/dev/null', '-c', '-isystem',
                                       vmlinux_h_dir, *bpfcc_flags],
                                      ebpf_source)
        if code:
            if tmp:
                tmp.cleanup()
            if suppress_error:
                return False
            info(TXT_MAGENTA(f'broken {buildopts.bpf_cc[0]}'))
            if code == -1:
                error(f'BPF compiler is not working: {stderr}')
            if code == -2:
                error(f'Unexpected error: {stderr}')
            info(f'BPF Compiler returned non-zero return code with message:\n{stderr}', end='')
            error('Can not compile a minimum unit of eBPF object, you may disable eBPF support')
        return True

    def _test_vmlinux(vmlinux: Path) -> bool:
        # print error when returning False
        try:
            with vmlinux.open('rb') as _:
                pass
        except Exception as e:
            info(TXT_BRIGHT_BLUE('unsupported kernel/vmlinux'))
            warn(f'Failed to read {vmlinux} due to {e}')
            buildopts.ebpf = False
            return False

        code, vmlinux_h, stderr = run_command([buildopts.bpftool, 'btf', 'dump', 'file',
                                               str(vmlinux), 'format', 'c'])
        if code:
            info(TXT_BRIGHT_BLUE(f'broken {buildopts.bpftool}'))
            info(f'bpftool returned non-zero return code with message:\n{stderr}', end='')
            warn('Provided can not dump vmlinux to vmlinux.h, you may disable eBPF support')
            buildopts.ebpf = False
            return False

        try:
            global tmpdir
            tmpdir = tempfile.TemporaryDirectory()
            header_dir = tmpdir.name
            with open(f'{header_dir}/vmlinux.h', 'w') as file:
                file.write(vmlinux_h)

            _test_bpfcc(header_dir, tmp=tmpdir) # no return if error
        except Exception as e:
            info(TXT_BRIGHT_BLUE('/tmp not available'))
            warn(f'Failed to dump vmlinux.h due to {e}')
            buildopts.ebpf = False
            return False

        return True

    passed = False
    default_vmlinux = Path('/sys/kernel/btf/vmlinux')
    if buildopts.vmlinuxdir == 'auto':
        vmlinux_h_dir = None
        # detects several places
        def _pick_path(vmlinux_h: str) -> bool:
            candidate = Path(vmlinux_h)
            if candidate.exists() and candidate.is_file():
                nonlocal vmlinux_h_dir
                vmlinux_h_dir = str(candidate.parent)
                return True
            return False

        if not ((buildopts.target and \
            _pick_path(f'/usr/include/{buildopts.target}/linux/bpf/vmlinux.h')) or \
            _pick_path('/usr/src/linux/vmlinux.h') or \
            _pick_path('/usr/include/bpf/vmlinux.h')):
            # fedora case?
            ksrcdir = Path('/usr/src/kernels')
            if ksrcdir.exists() and ksrcdir.is_dir():
                for kdir, _, _ in ksrcdir.iterdir():
                    if _pick_path(str(kdir / 'vmlinux.h')):
                        break

        tried = None
        if vmlinux_h_dir and (tried := _test_bpfcc(vmlinux_h_dir, True)):
            info(TXT_GREEN(f'{vmlinux_h_dir}/vmlinux.h'))
            vmlinux_h_flags.extend(['-isystem', vmlinux_h_dir])
            makefile.inject('VMLINUX', False)
        elif _test_vmlinux(default_vmlinux):
            info(TXT_GREEN(f'generated from {default_vmlinux} (default)'))
            makefile.inject('VMLINUX_FILE', str(default_vmlinux))
            makefile.inject('VMLINUX', True)
        if tried is False:
            warn(f'Not picking {vmlinux_h_dir}/vmlinux.h as BPF_CC can not include it')
        # if vmlinux can not be processed, the whole ebpf part will be erased later
    elif buildopts.vmlinuxdir == 'generated' and _test_vmlinux(default_vmlinux):
        info(TXT_GREEN(f'generated from {default_vmlinux} (default)'))
        makefile.inject('VMLINUX_FILE', str(default_vmlinux))
        makefile.inject('VMLINUX', True)
    else:
        # should be a directory containing vmlinux or vmlinux.h
        vmlinux_dir = Path(buildopts.vmlinuxdir)
        if (candidate := vmlinux_dir / 'vmlinux.h').exists() and candidate.is_file():
            if _test_bpfcc(str(vmlinux_dir)):
                info(TXT_CYAN(str(candidate)))
                vmlinux_h_flags.extend(['-isystem', str(vmlinux_dir)])
                makefile.inject('VMLINUX', False)
            else:
                error(f'BPF_CC can not include {candidate}')
        elif (candidate := vmlinux_dir / 'vmlinux').exists() and candidate.is_file():
            if _test_vmlinux(candidate):
                info(TXT_CYAN(f'generated from {candidate}'))
                makefile.inject('VMLINUX_FILE', str(candidate))
                makefile.inject('VMLINUX', True)
            else:
                error(f'{candidate} can not be processed correctly')
        else:
            error(f'Can not find available vmlinux or vmlinux.h in {vmlinux_dir}')


libbpf = '''
#include <bpf/libbpf.h>
#include <stdio.h>
int main(void) {
    puts(libbpf_version_string());
    return 0;
}
'''
libbpf_ebpf = '''
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
unsigned test(void) {
    return bpf_get_prandom_u32();
}
'''
@new_task
def check_libbpf(makefile: Makefile):
    pcheck('libbpf')
    if not buildopts.ebpf:
        info(TXT_BLUE('skipped'))
        makefile.inject('LIBBPF_FLAG', '')
        return

    global tmpdir
    link_flags = probe_library_flags('bpf')
    code, _, stderr = run_command([*buildopts.cc, '-x', 'c', '-',
                                   *link_flags, '-o', '/dev/null'],
                                  libbpf, push_sys_dir=True)
    if code:
        info(TXT_MAGENTA('no'))
        if tmpdir:
            tmpdir.cleanup()
        if code == -1:
            error(f'C compiler is not working: {stderr}')
        if code == -2:
            error(f'Unexpected error: {stderr}')
        info(f'Compiler returned non-zero return code with message:\n{stderr}', end='')
        error('Can not compile a minimum unit of libbpf, you may install it or disable eBPF support')

    bpfcc_flags = vmlinux_h_flags.copy()
    if not bpfcc_flags:
        bpfcc_flags.extend(['-isystem', tmpdir.name])
    if is_gcc_bpf:
        # bpf-gcc
        bpfcc_flags.extend(['-O2', '-std=gnu17', '-isystem', f'{sys.prefix}/local/include',
                            '-isystem', f'{sys.prefix}/include'])
    code, _, stderr = run_command([*buildopts.bpf_cc, '-x', 'c', '-',
                                   '-o', '/dev/null', '-c', *bpfcc_flags],
                                  libbpf_ebpf, push_sys_dir=True)
    if tmpdir:
        tmpdir.cleanup()
    if code:
        info(TXT_MAGENTA(f'broken {buildopts.bpf_cc[0]}'))
        if code == -1:
            error(f'BPF compiler is not working: {stderr}')
        if code == -2:
            error(f'Unexpected error: {stderr}')
        info(f'BPF Compiler returned non-zero return code with message:\n{stderr}', end='')
        error('Can not compile a minimum unit of libbpf, you may install it or disable eBPF support')

    info(TXT_GREEN('yes'))
    makefile.inject('LIBBPF_FLAG', ' '.join(link_flags))

    if is_gcc_bpf:
        code, stdout, _ = run_command([buildopts.bpf_cc[0], '-dumpversion'])
        if code or int(stdout.strip().split('.', 1)[0]) < 15:
            warn('Recommends gcc>=15 to compile bpf, gcc<15 may not work properly')

@new_task
def check_bpf_arch(makefile: Makefile):
    pcheck('eBPF architecture')
    if not buildopts.ebpf:
        info(TXT_BLUE('skipped'))
        makefile.inject('EBPF_ON', '0')
        makefile.inject('EBPF', False)
        return

    arch = platform.machine()
    le = True # little endian
    extra_flags = []
    if buildopts.bpf_cflags:
        extra_flags.append(buildopts.bpf_cflags)
    extra_flags.extend(vmlinux_h_flags)
    if is_gcc_bpf:
        extra_flags.extend(['-isystem', f'{sys.prefix}/local/include',
                            '-isystem', f'{sys.prefix}/include',
                            '-std=gnu17'])
    if buildopts.target:
        arch = buildopts.target.split('-', 1)[0]
    if 'x86_64' in arch or re.match(r'i\d86', arch):
        buildopts.bpf_arch = 'x86'
        if 'x86_64' in arch:
            extra_flags.append('-D__x86_64__')
    elif arch.startswith('loongarch'):
        buildopts.bpf_arch = 'loongarch'
    elif arch.startswith('riscv'):
        buildopts.bpf_arch = 'riscv'
    elif arch.startswith('aarch64'):
        buildopts.bpf_arch = 'arm64'
        le = 'be' not in arch
        extra_flags.append('-D__aarch64__')
    elif arch.startswith('arm'):
        buildopts.bpf_arch = 'arm'
        le = 'eb' not in arch
    elif arch.startswith('s390'):
        buildopts.bpf_arch = 's390'
        le = False
    elif arch.startswith('mips'):
        buildopts.bpf_arch = 'mips'
        le = 'el' in arch
    elif arch.startswith(('ppc', 'powerpc')):
        buildopts.bpf_arch = 'powerpc'
        le = 'le' in arch
    else:
        info(TXT_BRIGHT_BLUE(arch))
        warn(f'Specified architecture {arch} is not supported by eBPF')
        buildopts.ebpf = False
        makefile.inject('EBPF_ON', '0')
        makefile.inject('EBPF', False)
        return

    info(TXT_GREEN(f"{buildopts.bpf_arch}, {'little' if le else 'big'} endian"))
    makefile.inject('BPF_ARCH', buildopts.bpf_arch)
    makefile.inject('BPF_EXTRA_FLAG', ' '.join(extra_flags))
    if buildopts.bpf_cc[0].startswith('bpf'):
        if not le:
            buildopts.bpf_cc[-1] = '-mbig-endian'
    else:
        buildopts.bpf_cc[-1] = 'bpf' + 'el' if le else 'eb'
    makefile.inject('BPF_CC', ' '.join(buildopts.bpf_cc))
    makefile.inject('BPFTOOL', buildopts.bpftool)

    makefile.inject('LLVM_STRIP', not is_gcc_bpf and found_llvm_strip)

    makefile.inject('EBPF_ON', '1')
    makefile.inject('EBPF', True)

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

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