scripts/setup.py (210 lines of code) (raw):
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import argparse
import collections
import logging
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import List, Optional
LOG: logging.Logger = logging.getLogger(__name__)
def _path_exists(path: str) -> Path:
path = os.path.expanduser(path)
if not os.path.exists(path):
raise argparse.ArgumentTypeError(f"Path `{path}` does not exist.")
return Path(path)
def _directory_exists(path: str) -> Path:
path = os.path.expanduser(path)
if not os.path.isdir(path):
raise argparse.ArgumentTypeError(f"Path `{path}` is not a directory.")
return Path(path)
def _check_executable(path: Path) -> Path:
if not (path.exists() and os.access(path, os.X_OK)):
raise argparse.ArgumentTypeError(f"Path `{path}` is not executable.")
return path
def _executable_exists(path: str) -> Path:
return _check_executable(_path_exists(path))
def _check_python_directory(path: Path) -> Path:
init_path = path / "__init__.py"
if not init_path.exists():
raise argparse.ArgumentTypeError(f"Path `{init_path}` does not exist.")
return path
def _python_directory_exists(path: str) -> Path:
return _check_python_directory(_directory_exists(path))
def _run_python(arguments: List[str], working_directory: Optional[Path] = None) -> None:
LOG.info(f"Running python {' '.join(arguments)}")
subprocess.run([sys.executable] + arguments, check=True, cwd=working_directory)
def _install_build_dependencies() -> None:
_run_python(["-m", "pip", "install", "--upgrade", "pip", "build"])
def _prepare_build_directory(
build_root: Path,
package_name: str,
package_version: str,
repository: Path,
binary: Path,
pyredex: Path,
) -> None:
LOG.info(f"Preparing build at `{build_root}`")
_add_initial_files(build_root)
_sync_readme(build_root, repository)
_sync_python_files(build_root, repository, pyredex)
_sync_configuration_files(build_root, repository)
_sync_binary(build_root, binary)
_add_package_py(build_root, package_name, package_version)
_add_pyproject(build_root)
_add_setup_cfg(build_root, package_name, package_version)
def _build_package(build_root: Path) -> None:
_run_python(["-m", "build"], working_directory=build_root)
def _distribution_platform() -> str:
if sys.platform == "linux":
return "manylinux1_x86_64"
elif sys.platform == "darwin":
return "macosx_10_11_x86_64"
else:
raise AssertionError("This operating system is not currently supported.")
def _copy_package(build_root: Path, output_path: Path) -> None:
# Find the source distribution and wheel
dist_directory = build_root / "dist"
wheel = list(dist_directory.glob("**/*.whl"))
source_distribution = list(dist_directory.glob("**/*.tar.gz"))
if not len(wheel) == 1 and not len(source_distribution) == 1:
raise AssertionError(f"Unexpected files found in `{build_root}/dist`.")
source_distribution, wheel = source_distribution[0], wheel[0]
# Rename and move under the output path
output_path /= "dist"
output_path.mkdir(exist_ok=True)
shutil.move(
# pyre-fixme[6]: Expected `str` for 1st param but got `Path`.
wheel,
output_path
/ wheel.name.replace("-any.whl", f"-{_distribution_platform()}.whl"),
)
# pyre-fixme[6]: Expected `str` for 1st param but got `Path`.
shutil.move(source_distribution, output_path / source_distribution.name)
def _install_package(build_root: Path) -> None:
_run_python(["-m", "pip", "install", "."], working_directory=build_root)
def _mkdir_and_init(module_path: Path) -> None:
module_path.mkdir()
init_path = module_path / "__init__.py"
init_path.write_text(
"""\
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
"""
)
def _add_initial_files(build_root: Path) -> None:
_mkdir_and_init(build_root / "mariana_trench")
_mkdir_and_init(build_root / "mariana_trench" / "shim")
(build_root / "share").mkdir()
(build_root / "share" / "mariana-trench").mkdir()
def _copy(source_path: Path, target_path: Path) -> None:
LOG.info(f"Copying `{source_path}` into `{target_path}`")
shutil.copy(source_path, target_path)
def _sync_readme(build_root: Path, repository: Path) -> None:
_copy(repository / "README.md", build_root / "README.md")
def _rsync_files(
filters: List[str],
source_directory: Path,
target_directory: Path,
arguments: List[str],
) -> None:
LOG.info(f"Copying `{source_directory}` into `{target_directory}`")
command = ["rsync", "--quiet"]
command.extend(arguments)
command.extend(["--filter=" + filter_string for filter_string in filters])
command.append(str(source_directory))
command.append(str(target_directory))
subprocess.run(command, check=True)
def _sync_python_files(build_root: Path, repository: Path, pyredex: Path) -> None:
filters = ["- tests/", "+ */", "-! *.py"]
_rsync_files(filters, repository / "shim", build_root / "mariana_trench", ["-avm"])
_rsync_files(filters, pyredex, build_root, ["-avm"])
def _sync_configuration_files(build_root: Path, repository: Path) -> None:
filters = ["+ */", "-! *.json"]
_rsync_files(
filters,
repository / "configuration",
build_root / "share/mariana-trench",
["-avm"],
)
def _sync_binary(build_root: Path, binary: Path) -> None:
(build_root / "bin").mkdir()
_copy(binary, build_root / "bin" / "mariana-trench-binary")
def _add_package_py(build_root: Path, package_name: str, package_version: str) -> None:
path = build_root / "mariana_trench/shim/package.py"
path.write_text(
f"""\
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
name = '{package_name}'
version = '{package_version}'
"""
)
def _add_pyproject(build_root: Path) -> None:
LOG.info(f"Generating `{build_root}/pyproject.toml`")
pyproject = build_root / "pyproject.toml"
pyproject.write_text(
"""\
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"
"""
)
def _add_setup_cfg(
build_root: Path,
package_name: str,
package_version: str,
) -> None:
LOG.info(f"Generating `{build_root}/setup.cfg`")
configuration_directories = collections.defaultdict(list)
for path in build_root.glob("share/mariana-trench/**/*"):
if path.is_file():
path = path.relative_to(build_root)
configuration_directories[path.parent].append(path)
configuration_files = ""
for directory, paths in configuration_directories.items():
configuration_files += f"{directory} =\n"
for path in paths:
configuration_files += f" {path}\n"
setup_cfg = build_root / "setup.cfg"
setup_cfg.write_text(
f"""\
[metadata]
name = {package_name}
version = {package_version}
description = A security focused static analysis platform targeting Android.
long_description = file: README.md
long_description_content_type = text/markdown
url = https://mariana-tren.ch/
download_url = https://github.com/facebook/mariana-trench
author = Facebook
author_email = pyre@fb.com
maintainer = Facebook
maintainer_email = pyre@fb.com
license = MIT
keywords = security, taint, flow, static, analysis, android, java
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: MacOS
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: C++
Topic :: Security
Topic :: Software Development
Typing :: Typed
[options]
packages = find:
python_requires = >=3.6
install_requires =
pyre_extensions
fb-sapp
[options.entry_points]
console_scripts =
mariana-trench = mariana_trench.shim.shim:main
[options.data_files]
bin = bin/mariana-trench-binary
{configuration_files}
"""
)
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
parser = argparse.ArgumentParser()
parser.add_argument(
"--name", type=str, default="mariana-trench", help="Package name."
)
parser.add_argument(
"--version", type=str, default="0.dev0", help="Package version."
)
parser.add_argument(
"--repository",
type=_directory_exists,
default=".",
help="Path to the root of the repository.",
)
parser.add_argument(
"--binary",
type=_executable_exists,
help="Path to the analyzer binary.",
required=True,
)
parser.add_argument(
"--pyredex",
type=_python_directory_exists,
help="Path to the pyredex directory.",
required=True,
)
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser("build", help="Build the pypi package")
subparsers.add_parser("install", help="Install the pypi package")
arguments: argparse.Namespace = parser.parse_args()
repository: Path = arguments.repository
if (
not (repository / "README.md").exists()
and (repository / "../README.md").exists()
):
repository = repository / ".."
_install_build_dependencies()
with tempfile.TemporaryDirectory() as build_root:
build_path = Path(build_root)
_prepare_build_directory(
build_path,
arguments.name,
arguments.version,
repository,
arguments.binary,
arguments.pyredex,
)
if arguments.command == "build":
_build_package(build_path)
_copy_package(build_path, Path(os.getcwd()))
elif arguments.command == "install":
_install_package(build_path)
if __name__ == "__main__":
main()