#!/usr/bin/env -S python3 -B
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

import argparse
import enum
import logging
import os
import shutil
import subprocess
import sys
from pathlib import Path

logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO)


def find_command(names):
    """Search for command in `names`, and returns the first one that exists."""

    for name in names:
        path = shutil.which(name)
        if path is not None:
            return name

    return None


def assert_command(env_var, name):
    """Assert that the command is not empty
    The command name comes from either environment variable or find_command.
    """
    if not name:
        logging.error("{} command not found".format(env_var))
        sys.exit(1)


def parse_version(topsrc_dir):
    """Parse milestone.txt and return the entire milestone and major version."""
    milestone_file = topsrc_dir / "config" / "milestone.txt"
    if not milestone_file.is_file():
        return ("", "", "")

    with milestone_file.open("r") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            if line.startswith("#"):
                continue

            v = line.split(".")
            return tuple((v + ["", ""])[:3])

    return ("", "", "")


tmp_dir = Path("/tmp")

tar = os.environ.get("TAR", find_command(["tar"]))
assert_command("TAR", tar)

rsync = os.environ.get("RSYNC", find_command(["rsync"]))
assert_command("RSYNC", rsync)

m4 = os.environ.get("M4", find_command(["m4"]))
assert_command("M4", m4)

awk = os.environ.get("AWK", find_command(["awk"]))
assert_command("AWK", awk)

src_dir = Path(os.environ.get("SRC_DIR", Path(__file__).parent.absolute()))
mozjs_name = os.environ.get("MOZJS_NAME", "mozjs")
staging_dir = Path(os.environ.get("STAGING", tmp_dir / "mozjs-src-pkg"))
dist_dir = Path(os.environ.get("DIST", tmp_dir))
topsrc_dir = src_dir.parent.parent.absolute()

parsed_major_version, parsed_minor_version, parsed_patch_version = parse_version(
    topsrc_dir
)

major_version = os.environ.get("MOZJS_MAJOR_VERSION", parsed_major_version)
minor_version = os.environ.get("MOZJS_MINOR_VERSION", parsed_minor_version)
patch_version = os.environ.get("MOZJS_PATCH_VERSION", parsed_patch_version)
alpha = os.environ.get("MOZJS_ALPHA", "")

version = "{}-{}.{}.{}".format(
    mozjs_name, major_version, minor_version, patch_version or alpha or "0"
)
target_dir = staging_dir / version
package_name = "{}.tar.xz".format(version)
package_file = dist_dir / package_name
tar_opts = ["-Jcf"]

# Given there might be some external program that reads the following output,
# use raw `print`, instead of logging.
print("Environment:")
print("    TAR = {}".format(tar))
print("    RSYNC = {}".format(rsync))
print("    M4 = {}".format(m4))
print("    AWK = {}".format(awk))
print("    STAGING = {}".format(staging_dir))
print("    DIST = {}".format(dist_dir))
print("    SRC_DIR = {}".format(src_dir))
print("    MOZJS_NAME = {}".format(mozjs_name))
print("    MOZJS_MAJOR_VERSION = {}".format(major_version))
print("    MOZJS_MINOR_VERSION = {}".format(minor_version))
print("    MOZJS_PATCH_VERSION = {}".format(patch_version))
print("    MOZJS_ALPHA = {}".format(alpha))
print("")

rsync_filter_list = """
# Top-level config and build files

+ /aclocal.m4
+ /client.mk
+ /configure.py
+ /LICENSE
+ /mach
+ /Makefile.in
+ /moz.build
+ /moz.configure
+ /test.mozbuild
+ /.babel-eslint.rc.js
+ /.eslintignore
+ /.eslintrc.js
+ /.flake8
+ /.gitignore
+ /.hgignore
+ /.lldbinit
+ /.prettierignore
+ /.prettierrc
+ /.ycm_extra_conf.py

# Additional libraries (optionally) used by SpiderMonkey

+ /mfbt/**
+ /nsprpub/**

- /intl/icu/source/data
- /intl/icu/source/test
- /intl/icu/source/tools
+ /intl/icu/**

- /intl/components/gtest
+ /intl/components/**

+ /memory/replace/dmd/dmd.py
+ /memory/build/**
+ /memory/moz.build
+ /memory/mozalloc/**

+ /modules/fdlibm/**
+ /modules/zlib/**

+ /mozglue/baseprofiler/**
+ /mozglue/build/**
+ /mozglue/interposers/**
+ /mozglue/misc/**
+ /mozglue/moz.build
+ /mozglue/static/**

+ /tools/rb/fix_stacks.py
+ /tools/fuzzing/moz.build
+ /tools/fuzzing/interface/**
+ /tools/fuzzing/registry/**
+ /tools/fuzzing/libfuzzer/**
+ /tools/fuzzing/*.mozbuild

# Build system and dependencies

+ /Cargo.lock
+ /build/**
+ /config/**
+ /python/**

+ /.cargo/config.in

+ /third_party/function2/**
- /third_party/python/gyp
+ /third_party/python/**
+ /third_party/rust/**
+ /third_party/gemmology/**
+ /third_party/xsimd/**
+ /layout/tools/reftest/reftest/**

+ /testing/mach_commands.py
+ /testing/moz.build
+ /testing/mozbase/**
+ /testing/performance/**
+ /testing/web-platform/*.ini
+ /testing/web-platform/*.py
+ /testing/web-platform/meta/streams/**
+ /testing/web-platform/mozilla/**
+ /testing/web-platform/tests/resources/**
+ /testing/web-platform/tests/streams/**
+ /testing/web-platform/tests/tools/**

+ /toolkit/crashreporter/tools/symbolstore.py
+ /toolkit/mozapps/installer/package-name.mk

+ /xpcom/geckoprocesstypes_generator/**

# SpiderMonkey itself

+ /js/src/**
+ /js/app.mozbuild
+ /js/*.configure
+ /js/examples/**
+ /js/public/**

+ */
- /**
"""

INSTALL_CONTENT = """\
Documentation for SpiderMonkey is available at:

  https://spidermonkey.dev/

In particular, it points to build documentation at

  https://firefox-source-docs.mozilla.org/js/build.html

Note that the libraries produced by the build system include symbols,
causing the binaries to be extremely large. It is highly suggested that `strip`
be run over the binaries before deploying them.

Building with default options may be performed as follows:

  ./mach build

This will produce a debug build (much more suitable for developing against the
SpiderMonkey JSAPI). To produce an optimized build:

  export MOZCONFIG=$(pwd)/mozconfig.opt
  ./mach build

You may edit the mozconfig and mozconfig.opt files to configure your own build
appropriately.
"""

MOZCONFIG_DEBUG_CONTENT = """\
# Much slower when running, but adds assertions that are much better for
# developing against the JSAPI.
ac_add_options --enable-debug

# Much faster when running, worse for debugging.
ac_add_options --enable-optimize

mk_add_options MOZ_OBJDIR=obj-debug
"""

MOZCONFIG_OPT_CONTENT = """\
# Much faster when running, but very error-prone to develop against because
# this will skip all the assertions critical to using the JSAPI properly.
ac_add_options --disable-debug

# Much faster when running, worse for debugging.
ac_add_options --enable-optimize

mk_add_options MOZ_OBJDIR=obj-opt
"""

README_CONTENT = """\
This directory contains SpiderMonkey {major_version}.

This release is based on a revision of Mozilla {major_version}:
  https://hg.mozilla.org/releases/
The changes in the patches/ directory were applied.

See https://spidermonkey.dev/ for documentation, examples, and release notes.
""".format(
    major_version=major_version
)


def is_mozjs_cargo_member(line):
    """Checks if the line in workspace.members is mozjs-related"""

    return '"js/' in line


def is_mozjs_crates_io_local_patch(line):
    """Checks if the line in patch.crates-io is mozjs-related"""

    return any(f'path = "{p}' in line for p in ("js", "build", "third_party/rust"))


def clean():
    """Remove temporary directory and package file."""
    logging.info("Cleaning {} and {} ...".format(package_file, target_dir))
    if package_file.exists():
        package_file.unlink()
    if target_dir.exists():
        shutil.rmtree(str(target_dir))


def assert_clean():
    """Assert that target directory does not contain generated files."""
    makefile_file = target_dir / "js" / "src" / "Makefile"
    if makefile_file.exists():
        logging.error("found js/src/Makefile. Please clean before packaging.")
        sys.exit(1)


def create_target_dir():
    if target_dir.exists():
        logging.warning("dist tree {} already exists!".format(target_dir))
    else:
        target_dir.mkdir(parents=True)


def sync_files():
    # Output of the command should directly go to stdout/stderr.
    p = subprocess.Popen(
        [
            str(rsync),
            "--delete-excluded",
            "--prune-empty-dirs",
            "--quiet",
            "--recursive",
            "{}/".format(topsrc_dir),
            "{}/".format(target_dir),
            "--filter=. -",
        ],
        stdin=subprocess.PIPE,
    )

    p.communicate(rsync_filter_list.encode())

    if p.returncode != 0:
        sys.exit(p.returncode)


def copy_cargo_toml():
    cargo_toml_file = topsrc_dir / "Cargo.toml"
    target_cargo_toml_file = target_dir / "Cargo.toml"

    with cargo_toml_file.open("r") as f:

        class State(enum.Enum):
            BEFORE_MEMBER = 1
            INSIDE_MEMBER = 2
            AFTER_MEMBER = 3
            INSIDE_PATCH = 4
            AFTER_PATCH = 5

        content = ""
        state = State.BEFORE_MEMBER
        for line in f:
            if state == State.BEFORE_MEMBER:
                if line.strip() == "members = [":
                    state = State.INSIDE_MEMBER
            elif state == State.INSIDE_MEMBER:
                if line.strip() == "]":
                    state = State.AFTER_MEMBER
                elif not is_mozjs_cargo_member(line):
                    continue
            elif state == State.AFTER_MEMBER:
                if line.strip() == "[patch.crates-io]":
                    state = State.INSIDE_PATCH
            elif state == State.INSIDE_PATCH:
                if line.startswith("["):
                    state = State.AFTER_PATCH
                if "path = " in line:
                    if not is_mozjs_crates_io_local_patch(line):
                        continue

            content += line

    with target_cargo_toml_file.open("w") as f:
        f.write(content)


def generate_configure():
    """Generate configure files to avoid build dependency on autoconf-2.13"""

    src_old_configure_in_file = topsrc_dir / "js" / "src" / "old-configure.in"
    dest_old_configure_file = target_dir / "js" / "src" / "old-configure"

    js_src_dir = topsrc_dir / "js" / "src"

    env = os.environ.copy()
    env["M4"] = m4
    env["AWK"] = awk
    env["AC_MACRODIR"] = topsrc_dir / "build" / "autoconf"

    with dest_old_configure_file.open("w") as f:
        subprocess.run(
            [
                "sh",
                str(topsrc_dir / "build" / "autoconf" / "autoconf.sh"),
                "--localdir={}".format(js_src_dir),
                str(src_old_configure_in_file),
            ],
            stdout=f,
            check=True,
            env=env,
        )


def copy_file(filename, content):
    """Copy an existing file from the staging area, or create a new file
    with the given contents if it does not exist."""

    staging_file = staging_dir / filename
    target_file = target_dir / filename

    if staging_file.exists():
        shutil.copy2(str(staging_file), str(target_file))
    else:
        with target_file.open("w") as f:
            f.write(content)


def copy_patches():
    """Copy patches dir, if it exists."""

    staging_patches_dir = staging_dir / "patches"
    top_patches_dir = topsrc_dir / "patches"
    target_patches_dir = target_dir / "patches"

    if staging_patches_dir.is_dir():
        shutil.copytree(str(staging_patches_dir), str(target_patches_dir))
    elif top_patches_dir.is_dir():
        shutil.copytree(str(top_patches_dir), str(target_patches_dir))


def remove_python_cache():
    """Remove *.pyc and *.pyo files if any."""
    for f in target_dir.glob("**/*.pyc"):
        f.unlink()
    for f in target_dir.glob("**/*.pyo"):
        f.unlink()


def stage():
    """Stage source tarball content."""
    logging.info("Staging source tarball in {}...".format(target_dir))

    create_target_dir()
    sync_files()
    copy_cargo_toml()
    generate_configure()
    copy_file("INSTALL", INSTALL_CONTENT)
    copy_file("README", README_CONTENT)
    copy_file("mozconfig", MOZCONFIG_DEBUG_CONTENT)
    copy_file("mozconfig.opt", MOZCONFIG_OPT_CONTENT)
    copy_patches()
    remove_python_cache()


def create_tar():
    """Roll the tarball."""

    logging.info("Packaging source tarball at {}...".format(package_file))

    subprocess.run(
        [str(tar)] + tar_opts + [str(package_file), "-C", str(staging_dir), version],
        check=True,
    )


def build():
    assert_clean()
    stage()
    create_tar()


parser = argparse.ArgumentParser(description="Make SpiderMonkey source package")
subparsers = parser.add_subparsers(dest="COMMAND")
subparser_update = subparsers.add_parser("clean", help="")
subparser_update = subparsers.add_parser("build", help="")
args = parser.parse_args()

if args.COMMAND == "clean":
    clean()
elif not args.COMMAND or args.COMMAND == "build":
    build()
