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()