#!/usr/bin/env python3
"""Lists the libVLC sources and headers under the GPL rather than the LGPL that a MobileVLCKit
build compiled into what it links, and fails if there is any.

check-vlckit-licences.sh looks in the binary for the GPL code known by name; this finds code
no list names yet, such as a module a later VLCKit adds. It reads the build itself: the plugins
VLCKit's build links (Resources/MobileVLCKit/vlc-plugins-iPhone.xcconfig, written after its
blacklist), libVLC's core libraries, each one's objects from libVLC's generated Makefiles (with
the convenience libraries it takes in, as rotate takes in the motion library), and each object's
sources and the headers it included from its dependency file (as deinterlace included yadif.h).
A file counts as GPL when its licence comment names the GNU General Public License and not the
Lesser or Library one. Only libVLC's own sources are read; the contrib libraries' licences are
in Scripts/vlckit-licences.

    python3 Scripts/vlckit/find-gpl-sources.py <work-directory>/vlckit

Run by build-vlckit.sh after VideoLAN's build, for every slice it made.
"""

import os
import re
import sys
from pathlib import Path

SLICES = ("iPhoneOS/arm64", "iPhoneSimulator/arm64", "iPhoneSimulator/x86_64")
OWN_SOURCES = ("modules", "src", "include", "compat", "lib")
GPL = re.compile(r"GNU\s+(?:\*\s*|//\s*)?General\s+(?:\*\s*|//\s*)?Public")
NOT_GPL = re.compile(r"Lesser|Library\s+(?:\*\s*)?General")


def make_variables(makefile: Path) -> dict:
    """A generated Makefile's variables, continuation lines joined."""
    text = makefile.read_text(errors="replace").replace("\\\n", " ")
    variables = {}
    for line in text.splitlines():
        match = re.match(r"^([A-Za-z0-9_]+)\s*=\s*(.*)$", line)
        if match:
            variables[match.group(1)] = match.group(2)
    return variables


def expand(variables: dict, value: str, depth: int = 0) -> str:
    if depth > 20:
        return value
    return re.sub(r"\$\(([A-Za-z0-9_]+)\)",
                  lambda m: expand(variables, variables.get(m.group(1), ""), depth + 1), value)


def objects(variables: dict, library: str, seen: set) -> list:
    """A libtool library's objects, with those of the convenience libraries it takes in."""
    key = re.sub(r"[^A-Za-z0-9]", "_", library)
    found = expand(variables, variables.get(f"{key}_OBJECTS", "")).split()
    for added in expand(variables, variables.get(f"{key}_LIBADD", "")).split():
        name = os.path.basename(added)
        if added.endswith(".la") and name not in seen \
                and f"{re.sub(r'[^A-Za-z0-9]', '_', name)}_OBJECTS" in variables:
            seen.add(name)
            found += objects(variables, name, seen)
    return found


def sources(directory: Path, obj: str):
    """The files an object was compiled from, by its dependency file; None without one."""
    folder, name = os.path.split(obj)
    stem, suffix = os.path.splitext(name)
    deps = directory / folder / ".deps" / (stem + (".Plo" if suffix == ".lo" else ".Po"))
    if not deps.is_file():
        return None
    text = deps.read_text(errors="replace")
    if text.startswith("# dummy"):
        return None
    return {os.path.normpath(path) for path in re.findall(r"(/\S+\.(?:h|c|cpp|m|hpp|S|asm|inc))", text)}


def gpl_only(path: str) -> bool:
    try:
        head = Path(path).read_text(errors="replace")[:5000]
    except OSError:
        return False
    return bool(GPL.search(head)) and not NOT_GPL.search(head)


def main() -> None:
    if len(sys.argv) != 2:
        sys.exit(__doc__)
    vlckit = Path(sys.argv[1]).resolve()
    vlc = (vlckit / "libvlc/vlc").resolve()
    plugin_list = vlckit / "Resources/MobileVLCKit/vlc-plugins-iPhone.xcconfig"
    if not plugin_list.is_file():
        sys.exit(f"find-gpl-sources: no {plugin_list}: run VLCKit's build first")
    plugins = sorted(set(re.findall(r"(lib[A-Za-z0-9_]+_plugin)\.a", plugin_list.read_text())))
    own = tuple(f"{vlc / folder}/" for folder in OWN_SOURCES)
    found, unread = {}, []
    for slice_ in SLICES:
        platform, arch = slice_.split("/")
        build = vlc / f"build-{platform}" / arch
        if not build.is_dir():
            unread.append(f"{slice_}: no build")
            continue
        modules = make_variables(build / "modules/Makefile")
        libraries = [(f"{plugin}.a", build / "modules", objects(modules, f"{plugin}.la", set()))
                     for plugin in plugins]
        for folder, library in (("src", "libvlccore.la"), ("lib", "libvlc.la"), ("compat", "libcompat.la")):
            libraries.append((library, build / folder,
                              objects(make_variables(build / folder / "Makefile"), library, set())))
        for library, directory, objs in libraries:
            if not objs:
                unread.append(f"{slice_}: {library} has no objects")
            for obj in objs:
                files = sources(directory, obj)
                if files is None:
                    unread.append(f"{slice_}: {library}: no dependency file for {obj}")
                    continue
                for path in files:
                    if path.startswith(own) and gpl_only(path):
                        found.setdefault(path[len(str(vlc)) + 1:], set()).add(library)
    for message in unread:
        print(f"error: find-gpl-sources: {message}", file=sys.stderr)
    for path, libraries in sorted(found.items()):
        print(f"error: GPL source in MobileVLCKit: {path} ({', '.join(sorted(libraries))})", file=sys.stderr)
    if unread or found:
        sys.exit(1)
    print(f"find-gpl-sources: no GPL libVLC source among the {len(plugins)} plugins and the core, "
          f"in {len(SLICES)} slices")


if __name__ == "__main__":
    main()
