tracker-rs 0.1.0

Rust bindings for tracker
#!/usr/bin/env python3

from pathlib import Path
import argparse
import subprocess
import sys
import asyncio

DEFAULT_GIR_FILES_DIRECTORY = Path("./gir-files")
DEFAULT_GIR_DIRECTORY = Path("./gir/")
DEFAULT_GIR_PATH = DEFAULT_GIR_DIRECTORY / "target/release/gir"


def run_command(command, folder=None):
    return subprocess.run(command, cwd=folder, check=True)


async def spawn_process(exe, args):
    p = await asyncio.create_subprocess_exec(
        str(exe),
        *(str(arg) for arg in args),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )

    stdout, stderr = await p.communicate()
    stdout = stdout.decode("utf-8")
    stderr = stderr.decode("utf-8")
    assert p.returncode == 0, stderr.strip()
    return stdout, stderr


async def spawn_gir(gir_exe, args):
    stdout, stderr = await spawn_process(gir_exe, args)
    # Gir doesn't print anything to stdout. If it does, this is likely out of
    # order with stderr, unless the printer/logging flushes in between.
    assert not stdout, "`gir` printed unexpected stdout: {}".format(stdout)
    if stderr:
        return "===> stderr:\n\n" + stderr + "\n"
    return ""


def update_workspace():
    return run_command(["cargo", "build", "--release"], "gir")


def ask_yes_no_question(question, conf):
    question = "{} [y/N] ".format(question)
    if conf.yes:
        print(question + "y")
        return True
    line = input(question)
    return line.strip().lower() == "y"


def update_submodule(submodule_path, conf):
    if any(submodule_path.iterdir()):
        return False
    print("=> Initializing {} submodule...".format(submodule_path))
    run_command(["git", "submodule", "update", "--init", submodule_path])
    print("<= Done!")

    if ask_yes_no_question(
        "Do you want to update {} submodule?".format(submodule_path), conf
    ):
        print("=> Updating submodule...")
        run_command(["git", "reset", "--hard", "HEAD"], submodule_path)
        run_command(["git", "pull", "-f", "origin", "master"], submodule_path)
        print("<= Done!")
        return True
    return False


def build_gir():
    print("=> Building gir...")
    update_workspace()
    print("<= Done!")


async def regenerate_crate_docs(conf, crate_dir, base_gir_args):
    doc_path = "docs.md"
    # Generate into docs.md instead of the default vendor.md
    doc_args = base_gir_args + ["-m", "doc", "--doc-target-path", doc_path]

    # The above `gir -m doc` generates docs.md relative to the directory containing Gir.toml
    doc_path = crate_dir / doc_path
    embed_args = ["-m", "-d", crate_dir / "src"]

    logs = ""

    if conf.strip_docs:
        logs += "==> Stripping documentation from `{}`...\n".format(crate_dir)
        # -n dumps stripped docs to stdout
        _, stderr = await spawn_process("rustdoc-stripper", embed_args + ["-s", "-n"])
        if stderr:
            logs += "===> stderr:\n\n" + stderr + "\n"

    if conf.embed_docs:
        logs += "==> Regenerating documentation for `{}` into `{}`...\n".format(
            crate_dir, doc_path
        )
        logs += await spawn_gir(conf.gir_path, doc_args)

        logs += "==> Embedding documentation from `{}` into `{}`...\n".format(
            doc_path, crate_dir
        )
        stdout, stderr = await spawn_process(
            "rustdoc-stripper", embed_args + ["-g", "-o", doc_path]
        )
        if stdout:
            logs += "===> stdout:\n\n" + stdout + "\n"
        if stderr:
            logs += "===> stderr:\n\n" + stderr + "\n"

    return logs


def regen_crates(path, conf):
    processes = []
    if path.is_dir():
        for entry in path.rglob("Gir*.toml"):
            processes += regen_crates(entry, conf)
    elif path.match("Gir*.toml"):
        args = ["-c", path, "-o", path.parent] + [
            d for path in conf.gir_files_paths for d in ("-d", path)
        ]

        is_sys_crate = path.parent.name.endswith("sys")

        if conf.embed_docs or conf.strip_docs:
            # Embedding documentation only applies to non-sys crates
            if is_sys_crate:
                return processes

            processes.append(regenerate_crate_docs(conf, path.parent, args))
        else:
            if is_sys_crate:
                args.extend(["-m", "sys"])

            async def regenerate_crate(path, args):
                return "==> Regenerating `{}`...\n".format(path) + await spawn_gir(
                    conf.gir_path, args
                )

            processes.append(regenerate_crate(path, args))

    else:
        raise Exception("`{}` is not a valid Gir*.toml file".format(path))

    return processes


def valid_path(path):
    path = Path(path)
    if not path.exists():
        raise argparse.ArgumentTypeError("`{}` no such file or directory".format(path))
    return path


def directory_path(path):
    path = Path(path)
    if not path.is_dir():
        raise argparse.ArgumentTypeError("`{}` directory not found".format(path))
    return path


def file_path(path):
    path = Path(path)
    if not path.is_file():
        raise argparse.ArgumentTypeError("`{}` file not found".format(path))
    return path


def parse_args():
    parser = argparse.ArgumentParser(
        description="Helper to regenerate gtk-rs crates using gir.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )

    parser.add_argument(
        "path",
        nargs="*",
        default=[Path(".")],
        type=valid_path,
        help="Paths in which to look for Gir.toml files",
    )
    parser.add_argument(
        "--gir-files-directories",
        nargs="+",  # If the option is used, we expect at least one folder!
        dest="gir_files_paths",
        default=[],
        type=directory_path,
        help="Path of the gir-files folder",
    )
    parser.add_argument(
        "--gir-path",
        default=DEFAULT_GIR_PATH,
        type=file_path,
        help="Path of the gir executable to run",
    )
    parser.add_argument(
        "--yes",
        action="store_true",
        help=" Always answer `yes` to any question asked by the script",
    )
    parser.add_argument(
        "--no-fmt",
        action="store_true",
        help="If set, this script will not run `cargo fmt`",
    )
    parser.add_argument(
        "--embed-docs",
        action="store_true",
        help="Build documentation with `gir -m doc`, and embed it with `rustdoc-stripper -g`",
    )
    parser.add_argument(
        "--strip-docs",
        action="store_true",
        help="Remove documentation with `rustdoc-stripper -s -n`. Can be used in conjunction with --embed-docs",
    )

    return parser.parse_args()


async def main():
    conf = parse_args()

    if not conf.gir_files_paths:
        update_submodule(DEFAULT_GIR_FILES_DIRECTORY, conf)

    if conf.gir_path == DEFAULT_GIR_PATH:
        update_submodule(DEFAULT_GIR_DIRECTORY, conf)
        build_gir()

    print("=> Regenerating crates...")
    for path in conf.path:
        print("=> Looking in path `{}`".format(path))
        # Collect and print the results as soon as they trickle in, one process at a time:
        for coro in asyncio.as_completed(regen_crates(path, conf)):
            print(await coro, end="")

    if not conf.no_fmt and not run_command(["cargo", "fmt"]):
        return 1
    print("<= Done!")
    print("Don't forget to check if everything has been correctly generated!")
    return 0


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except Exception as e:
        print("Error: {}".format(e), file=sys.stderr)
        sys.exit(1)