#!/usr/bin/python3
"""
Wrapper around cargo to have it build using Debian settings.

Usage:
    export CARGO_HOME=debian/cargo_home
    cargo prepare-debian /path/to/local/registry
    cargo build
    cargo test
    cargo install

See cargo:d/rules and dh-cargo:cargo.pm for more examples.

Make sure you add "Build-Depends: python3:native" if you use this directly.
OTOH, you only need "Build-Depends: dh-cargo" if you use that.

If CARGO_HOME doesn't end with debian/cargo_home, then this script does nothing
and passes through directly to cargo.

Otherwise, you *must* set the following environment variables:

- DEB_CARGO_CRATE
  ${crate}_${version} of whatever you're building.

- DEB_CARGO_PACKAGE
  Debian binary package that the result is to be installed into.

- DEB_CARGO_CRATE_IN_REGISTRY
  Whether the crate is in the local-registry (1) or the cwd (0, empty).

- CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
- DEB_HOST_GNU_TYPE DEB_HOST_RUST_TYPE
- (optional) DEB_BUILD_OPTIONS DEB_BUILD_PROFILES

Due to https://github.com/rust-lang/cargo/issues/6338 you should probably
*unset* RUSTFLAGS.
"""

import os
import os.path
import shutil
import subprocess
import sys

FLAGS = "CFLAGS CXXFLAGS CPPFLAGS LDFLAGS"
ARCHES = "DEB_HOST_GNU_TYPE DEB_HOST_RUST_TYPE"

def log(*args):
    print("debian cargo wrapper:", *args, file=sys.stderr)

def logrun(*args, **kwargs):
    log("running subprocess", args, kwargs)
    return subprocess.run(*args, **kwargs)

def sourcepath(p=None):
    return os.path.join(os.getcwd(), p) if p else os.getcwd()

def prepare_debian(cargo_home, registry, cratespec, host_gnu_type, ldflags):
    if not os.path.exists(sourcepath(registry)):
        raise ValueError("non-existent registry: %s" % registry)

    rustflags = "-C debuginfo=2 --cap-lints warn".split()
    rustflags.extend(["-C", "linker=%s-gcc" % host_gnu_type])
    for f in ldflags:
        rustflags.extend(["-C", "link-arg=%s" % f])
    rustflags.extend(["--remap-path-prefix",
        "%s=/usr/share/cargo/registry/%s" % (sourcepath(), cratespec.replace("_", "-"))])

    # TODO: we cannot enable this until dh_shlibdeps works correctly; atm we get:
    # dpkg-shlibdeps: warning: can't extract name and version from library name 'libstd-XXXXXXXX.so'
    # and the resulting cargo.deb does not depend on the correct version of libstd-rust-1.XX
    # We probably need to add override_dh_makeshlibs to d/rules of rustc
    #rustflags.extend(["-C", "prefer-dynamic"])

    os.makedirs(cargo_home, exist_ok=True)
    with open("%s/config" % cargo_home, "w") as fp:
        fp.write("""[source.crates-io]
replace-with = "dh-cargo-registry"

[source.dh-cargo-registry]
directory = "{0}"

[build]
rustflags = {1}
""".format(sourcepath(registry), repr(rustflags)))

    return 0

def install(binpkg, cratespec, host_rust_type, crate_in_registry, *args):
    crate, version = cratespec.rsplit("_", 1)
    install_target = sourcepath("debian/%s/usr" % binpkg)
    logrun(["env", "RUST_BACKTRACE=1",
        # set CARGO_TARGET_DIR so build products are saved in target/
        # normally `cargo install` deletes them when it exits
        "CARGO_TARGET_DIR=" + sourcepath("target"),
        "/usr/bin/cargo"] + list(args) +
        ([crate, "--vers", version] if crate_in_registry else ["--path", sourcepath()]) +
        ["--root", install_target], check=True)
    logrun(["rm", "-f", "%s/.crates.toml" % install_target])

    # if there was a custom build output, symlink it to debian/cargo_out_dir
    # hopefully cargo will provide a better solution in future https://github.com/rust-lang/cargo/issues/5457
    r = logrun('''ls -td "target/%s/release/build/%s"-*/out 2>/dev/null | head -n1'''
        % (host_rust_type, crate), shell=True, stdout=subprocess.PIPE).stdout
    r = r.decode("utf-8").rstrip()
    if r:
        logrun(["ln", "-sfT", "../%s" % r, "debian/cargo_out_dir"], check=True)
    return 0

def main(*args):
    cargo_home = os.getenv("CARGO_HOME", "")
    if not cargo_home.endswith("/debian/cargo_home"):
        os.execv("/usr/bin/cargo", ["cargo"] + list(args))

    if any(f not in os.environ for f in FLAGS.split()):
        raise ValueError("not all of %s set; did you call dpkg-buildflags?" % FLAGS)

    if any(f not in os.environ for f in ARCHES.split()):
        raise ValueError("not all of %s set; did you include architecture.mk?" % ARCHES)

    build_options = os.getenv("DEB_BUILD_OPTIONS", "").split()
    build_profiles = os.getenv("DEB_BUILD_PROFILES", "").split()

    parallel = []
    for o in build_options:
        if o.startswith("parallel="):
            parallel = ["-j" + o[9:]]
    nodoc = "nodoc" in build_options or "nodoc" in build_profiles
    nocheck = "nocheck" in build_options or "nocheck" in build_profiles

    # note this is actually the "build target" type, see rustc's README.Debian
    # for full details of the messed-up terminology here
    host_rust_type = os.getenv("DEB_HOST_RUST_TYPE", "")
    host_gnu_type = os.getenv("DEB_HOST_GNU_TYPE", "")

    log("options, profiles, parallel:", build_options, build_profiles, parallel)
    log("rust_type, gnu_type:", ", ".join([host_rust_type, host_gnu_type]))

    if "RUSTFLAGS" in os.environ:
        log("\033[33;1mWARNING: RUSTFLAGS is set; this will probably override all Debian rust settings.\033[0m")
        log("\033[33;1mWARNING: It is highly recommended to unset it; please see https://github.com/rust-lang/cargo/issues/6338 for details.\033[0m")

    if args[0] == "prepare-debian":
        registry = args[1]
        return prepare_debian(cargo_home, registry,
            os.environ["DEB_CARGO_CRATE"], host_gnu_type, os.getenv("LDFLAGS", "").split())

    newargs = []
    subcmd = None
    for a in args:
        if a in ("build", "rustc", "doc", "test", "bench", "install"):
            subcmd = a
            newargs.extend(["-Zavoid-dev-deps", a, "--verbose", "--verbose"] +
                parallel + ["--target", host_rust_type])
        elif a == "clean":
            subcmd = a
            newargs.extend([a, "--verbose", "--verbose"])
        else:
            newargs.append(a)

    if nodoc and subcmd == "doc":
        return 0
    if nocheck and subcmd in ("test", "bench"):
        return 0

    if subcmd == "clean":
        logrun(["env", "RUST_BACKTRACE=1", "/usr/bin/cargo"] + list(newargs), check=True)
        if os.path.exists(cargo_home):
            shutil.rmtree(cargo_home)
        return 0

    cargo_config = "%s/config" % cargo_home
    if not os.path.exists(cargo_config):
        raise ValueError("does not exist: %s, did you run `cargo prepare-debian <registry>`?" % cargo_config)

    if subcmd == "install":
        return install(os.environ["DEB_CARGO_PACKAGE"],
            os.environ["DEB_CARGO_CRATE"],
            host_rust_type,
            os.getenv("DEB_CARGO_CRATE_IN_REGISTRY", "") == "1",
            *newargs)
    else:
        return logrun(["env", "RUST_BACKTRACE=1", "/usr/bin/cargo"] + list(newargs)).returncode

if __name__ == "__main__":
    sys.exit(main(*sys.argv[1:]))

