scripts/setup.py (297 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 logging import multiprocessing import os import shutil import subprocess import sys from enum import Enum from pathlib import Path from tempfile import mkdtemp from typing import Dict, List, Mapping, NamedTuple, Optional, Type LOG: logging.Logger = logging.getLogger(__name__) COMPILER_VERSION = "4.10.2" DEVELOPMENT_COMPILER: str = COMPILER_VERSION RELEASE_COMPILER = f"{COMPILER_VERSION}+flambda" DEPENDENCIES = [ "base64.3.5.0", "core.v0.14.1", "re2.v0.14.0", "dune.2.9.1", "yojson.1.7.0", "ppx_deriving_yojson.3.6.1", "ounit.2.2.4", "menhir.20211230", "lwt.5.5.0", "ounit2-lwt.2.2.4", "pyre-ast.0.1.8", "mtime.1.3.0", ] class OCamlbuildAlreadyInstalled(Exception): pass class OldOpam(Exception): pass class BuildType(Enum): EXTERNAL = "external" FACEBOOK = "facebook" def _custom_linker_option(pyre_directory: Path, build_type: BuildType) -> str: # HACK: This is a temporary workaround for inconsistent OS installations # in FB-internal CI. Can be removed once all fleets are upgraded. if build_type == BuildType.FACEBOOK and sys.platform == "linux": return ( (pyre_directory / "facebook" / "scripts" / "custom_linker_options.txt") .read_text() .rstrip() ) else: return "" class Setup(NamedTuple): opam_root: Path development: bool = False release: bool = False @property def compiler_override(self) -> Optional[str]: if self.development: return DEVELOPMENT_COMPILER if self.release: return RELEASE_COMPILER return None @property def compiler(self) -> str: return self.compiler_override or DEVELOPMENT_COMPILER @property def make_arguments(self) -> str: if self.release: return "release" else: return "dev" @property def environment_variables(self) -> Mapping[str, str]: return os.environ def produce_dune_file( self, pyre_directory: Path, build_type: Optional[BuildType] = None ) -> None: if not build_type: if (pyre_directory / "facebook").is_dir(): build_type = BuildType.FACEBOOK else: build_type = BuildType.EXTERNAL with open(pyre_directory / "source" / "dune.in") as dune_in: with open(pyre_directory / "source" / "dune", "w") as dune: dune_data = dune_in.read() dune.write( dune_data.replace("%VERSION%", build_type.value).replace( "%CUSTOM_LINKER_OPTION%", _custom_linker_option(pyre_directory, build_type), ) ) def check_if_preinstalled(self) -> None: if self.environment_variables.get( "CHECK_IF_PREINSTALLED" ) != "false" and shutil.which("ocamlc"): ocamlc_location = self.run(["ocamlc", "-where"]) test_ocamlbuild_location = Path(ocamlc_location) / "ocamlbuild" if test_ocamlbuild_location.is_dir(): LOG.error( "OCamlbuild will refuse to install since it is already " + f"present at {test_ocamlbuild_location}." ) LOG.error("If you want to bypass this safety check, run:") LOG.error("CHECK_IF_PREINSTALLED=false ./scripts/setup.sh") raise OCamlbuildAlreadyInstalled def validate_opam_version(self) -> None: version = self.run(["opam", "--version"]) if version[:1] != "2": LOG.error( "Pyre only supports opam 2.0.0 and above, please update your " + "opam version." ) raise OldOpam def opam_environment_variables(self) -> Dict[str, str]: LOG.info("Activating opam") opam_env_result = self.run( [ "opam", "env", "--yes", "--switch", self.compiler, "--root", self.opam_root.as_posix(), "--set-root", "--set-switch", ] ) opam_environment_variables: Dict[str, str] = {} # `opam env` produces lines of two forms: # - comments like ": this comment, starts with a colon;" # - lines defining and exporting env vars like "ENV_VAR=value; export ENV_VAR;" for line in opam_env_result.split("\n"): if not line.startswith(":"): environment_variable, quoted_value = line.split(";")[0].split("=") value = quoted_value[1:-1] LOG.info(f'{environment_variable}="{value}"') opam_environment_variables[environment_variable] = value return opam_environment_variables def initialize_opam_switch(self) -> Mapping[str, str]: self.check_if_preinstalled() self.validate_opam_version() self.run( [ "opam", "init", "--bare", "--yes", "--root", self.opam_root.as_posix(), "default", "https://opam.ocaml.org", ] ) self.run( [ "opam", "switch", "create", self.compiler, "--packages=ocaml-option-flambda", "--yes", "--root", self.opam_root.as_posix(), ] ) opam_environment_variables = self.opam_environment_variables() # This is required to work around a bug in pre-OCaml 4.12 that prevents # `re2` from installing correctly. # See https://github.com/janestreet/re2/issues/31 ocamlc_location = self.run( ["ocamlc", "-where"], add_environment_variables=opam_environment_variables ) self.run(["rm", "-f", f"{ocamlc_location}/version"]) self.run( ["opam", "install", "--yes"] + DEPENDENCIES, add_environment_variables=opam_environment_variables, ) return opam_environment_variables def set_opam_switch_and_install_dependencies(self) -> Mapping[str, str]: self.run( [ "opam", "switch", "set", self.compiler, "--root", self.opam_root.as_posix(), ] ) opam_environment_variables = self.opam_environment_variables() self.run( ["opam", "install", "--yes"] + DEPENDENCIES, add_environment_variables=opam_environment_variables, ) return opam_environment_variables def full_setup( self, pyre_directory: Path, *, run_tests: bool = False, run_clean: bool = False, build_type_override: Optional[BuildType] = None, ) -> None: self.produce_dune_file(pyre_directory, build_type_override) opam_environment_variables = self.set_opam_switch_and_install_dependencies() if run_clean: self.run( ["dune", "clean"], pyre_directory / "source", add_environment_variables=opam_environment_variables, ) jobs = str(multiprocessing.cpu_count()) if run_tests: self.run( ["make", "--jobs", jobs, "test", "--directory", "source"], pyre_directory, add_environment_variables=opam_environment_variables, ) self.run( ["make", self.make_arguments, "--jobs", jobs, "--directory", "source"], pyre_directory, add_environment_variables=opam_environment_variables, ) def run( self, command: List[str], current_working_directory: Optional[Path] = None, add_environment_variables: Optional[Mapping[str, str]] = None, ) -> str: if add_environment_variables: environment_variables = { **self.environment_variables, **add_environment_variables, } else: environment_variables = self.environment_variables LOG.info(command) output = subprocess.check_output( command, universal_newlines=True, cwd=current_working_directory, env=environment_variables, ) if output.endswith("\n"): return output[:-1] else: return output def _make_opam_root(local: bool, temporary_root: bool, default: Optional[Path]) -> Path: home = Path.home() home_opam = home / ".opam" if local: if not home_opam.is_dir(): local_opam = home / "local" / "opam" local_opam.mkdir(parents=True) local_opam.symlink_to(home_opam, target_is_directory=True) return home_opam if temporary_root: return Path(mkdtemp()) return default or home_opam def setup(runner_type: Type[Setup]) -> None: logging.basicConfig( level=logging.INFO, format="[%(asctime)s] [%(levelname)s] %(message)s" ) parser = argparse.ArgumentParser(description="Set up Pyre.") parser.add_argument("--pyre-directory", type=Path) parser.add_argument("--local", action="store_true") parser.add_argument("--temporary_root", action="store_true") parser.add_argument("--opam-root", type=Path) parser.add_argument("--configure", action="store_true") parser.add_argument("--environment-only", action="store_true") parser.add_argument("--development", action="store_true") parser.add_argument("--release", action="store_true") parser.add_argument("--build-type", type=BuildType) parser.add_argument("--no-tests", action="store_true") parsed = parser.parse_args() pyre_directory = parsed.pyre_directory if not pyre_directory: pyre_directory = Path(__file__).parent.parent.absolute() opam_root = _make_opam_root(parsed.local, parsed.temporary_root, parsed.opam_root) runner = runner_type( opam_root=opam_root, development=parsed.development, release=parsed.release ) if parsed.configure: runner.produce_dune_file(pyre_directory, parsed.build_type) compiler_override = runner.compiler_override if compiler_override: runner.run( [ "opam", "switch", "set", compiler_override, "--root", runner.opam_root.as_posix(), ] ) elif parsed.environment_only: runner.produce_dune_file(pyre_directory, parsed.build_type) runner.initialize_opam_switch() LOG.info("Environment built successfully, stopping here as requested.") else: runner.full_setup( pyre_directory, run_tests=not parsed.no_tests, build_type_override=parsed.build_type, ) if __name__ == "__main__": setup(Setup)