#!/usr/bin/env python3

# This file is the Semgrep CLI entry point of the Semgrep pip package,
# the Semgrep HomeBrew package, and the Semgrep Docker container.
#
# In the future we may have different entry points when packaging Semgrep
# with Cargo, Npm, Opam, or even with Docker
# (ideally the entry point would be src/main/Main.ml without
#  any wrapper around once osemgrep is finished).
#
# The main purpose of this small wrapper is to dispatch
# either to the legacy pysemgrep (see the pysemgrep script in this
# directory), or to the new osemgrep (accessible via the semgrep-core binary
# under cli/src/semgrep/bin/ or somewhere in the PATH).
#
# It would be faster and cleaner to have a Bash script instead of a Python
# script here, but actually the overhead of Python here is just 0.015s.
# Moreover, it is sometimes hard from a Bash script to find where is installed
# semgrep-core, but it is simple from Python because you can simply use
# importlib.resources. We could also use 'pip show semgrep' from a Bash script
# to find semgrep-core, but will 'pip' be in the PATH? Should we use 'pip' or
# 'pip3'?
# Again, it is simpler to use a Python script and leverage importlib.resources.
# Another alternative would be to always have semgrep-core in the PATH,
# but when trying to put this binary in cli/bin, setuptools is yelling
# and does not know what to do with it. In the end, it is simpler to use
# a *Python* script when installed via a *Python* package manager (pip).

import os
import sys
import importlib.resources
import shutil

#alt: you can also add '-W ignore::DeprecationWarning' after the python3 above,
# but setuptools and pip adjust this line when installing semgrep so we need
# to do this instead.
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

# Add the directory containing this script in the PATH, so the pysemgrep
# script will also be in the PATH.
# Some people don't have semgrep in their PATH and call it instead
# explicitly as in /path/to/somewhere/bin/semgrep, but this means
# that calling pysemgrep from osemgrep would be difficult because
# it would not be in the PATH (we would need to pass its path to osemgrep,
# which seems more complicated).
PATH = os.environ.get("PATH", "")
os.environ["PATH"] = PATH + os.pathsep + os.path.dirname(os.path.abspath(__file__))
           
# similar to cli/src/semgrep/semgrep_core.py compute_executable_path()
def find_semgrep_core_path():
    # First, try the packaged binary.
    try:
        # the use of .path causes a DeprecationWarning hence the
        # filterwarnings above
        with importlib.resources.path("semgrep.bin", "semgrep-core") as path:
            if path.is_file():
                return str(path)
    except FileNotFoundError as e:
        pass

    # Second, try in PATH. In certain context such as Homebrew
    # (see https://github.com/Homebrew/homebrew-core/blob/master/Formula/semgrep.rb)
    # or Docker (see ../../Dockerfile), we actually copy semgrep-core in
    # /usr/local/bin (or in a bin/ folder in the PATH). In those cases,
    # there is no /.../site-packages/semgrep-xxx/bin/semgrep-core.
    # In those cases, we want to grab semgrep-core from the PATH instead.
    path = shutil.which("semgrep-core")
    if path is not None:
        return path
 
    print(f"Failed to find semgrep-core in PATH or in the semgrep package.",
          file=sys.stderr)
    # fatal error, see src/osemgrep/core/Exit_code.ml
    sys.exit(2)

# We could have moved the code below in a separate 'osemgrep' file, like
# for 'pysemgrep', but we don't want users to be exposed to another command,
# so it is better to hide it.
# We expose 'pysemgrep' because osemgrep itself might need to fallback to
# pysemgrep and it's better to avoid the possibility of an infinite loop
# by simply using a different program name. Morever, in case of big problems,
# we can always tell users to run pysemgrep instead of semgrep and be sure
# they'll get the old behavior.
def exec_osemgrep():
    path = find_semgrep_core_path()
    # If you call semgrep-core as osemgrep, then we get osemgrep behavior,
    # see src/main/Main.ml
    sys.argv[0] = "osemgrep"
    os.execvp(str(path), sys.argv)


# When python imports a module, i.e. `import semgrep.FOO`, it runs the module's
# top-level code, which is the code that is not inside any function or class.
# This means that if we try and run a function from the semgrep package in a
# subprocess, we would end up calling os.execvp() above or main() below, which
# cause a bunch of errors. So we need to make sure that we only run the code
# below when this file is run as an executable, hence the guard below.
# This fix a crash when using semgrep --test (which apparently triggers
# execution of this file).
# Austin: the above explanation is my understanding, but I think Python
# is weirder under the hood with this stuff.
# Pad: I don't understand why the use of '--test' triggers the code here;
# if '--test' use semgrep.Foo, it should execute code in cli/src/semgrep/__init__.php
# This file is not part of the Python 'semgrep' package; it's a script.
if __name__ == "__main__":
    # escape hatch for users to pysemgrep in case of problems (they
    # can also call directly 'pysemgrep').
    if "--legacy" in sys.argv:
       sys.argv.remove("--legacy")
       #TODO: we should just do 'execvp("pysemgrep", sys.argv)'
       # but this causes some regressions with --test (see PA-2963)
       # and autocomplete (see #8359)
       #TODO: we should get rid of autocomplete anyway (it's a Python Click
       # thing not supported by osemgrep anyway),
       #TODO: we should fix --test instead.
       # The past investigation of Austin is available in #8360 PR comments
       import semgrep.__main__
       sys.exit(semgrep.__main__.main())
    elif "--experimental" in sys.argv:
       exec_osemgrep()
    else:
       # we now default to osemgrep! but this will usually exec
       # back to pysemgrep for most commands (for now)
       exec_osemgrep()
