| #!/usr/bin/env python3 |
| |
| # Copyright 2021 Efabless Corporation |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import re |
| import os |
| import sys |
| import uuid |
| import tempfile |
| import pathlib |
| import textwrap |
| import subprocess |
| from os.path import join, abspath, dirname, exists, realpath |
| from typing import Tuple, Union, List |
| |
| openlane_dir = dirname(dirname(abspath(__file__))) |
| is_root = os.geteuid() == 0 |
| |
| |
| class chdir(object): |
| def __init__(self, path): |
| self.path = path |
| self.previous = None |
| |
| def __enter__(self): |
| self.previous = os.getcwd() |
| os.chdir(self.path) |
| |
| def __exit__(self, exc_type, exc_value, traceback): |
| os.chdir(self.previous) |
| if exc_type is not None: |
| raise exc_value |
| |
| |
| def sh(*args: Tuple[str], root: Union[bool, str] = False, **kwargs): |
| """ |
| args: shell arguments to run |
| root: |
| if False, the command will be executed as-is |
| if True, if the user is not root, "sudo" will be added to the command |
| if "retry", the command will be executed as-is first, and if it fails, |
| it is retried as root. |
| """ |
| args = list(args) |
| if root and not is_root: |
| args = ["sudo"] + args |
| try: |
| subprocess.run( |
| args, |
| check=True, |
| stderr=subprocess.PIPE if root == "retry" else None, |
| **kwargs, |
| ) |
| except subprocess.CalledProcessError as e: |
| if root == "retry": |
| args = ["sudo"] + args |
| subprocess.run(args, check=True, **kwargs) |
| else: |
| raise e |
| |
| |
| def download(url: str, ext: str) -> str: |
| path = f"/tmp/{uuid.uuid4()}.{ext}" |
| print(f"{url} -> {path}") |
| target = open(path, "wb") |
| sh("curl", "-L", url, stdout=target) |
| target.close() |
| return path |
| |
| |
| # Installer Class |
| class Installer(object): |
| def __init__(self): |
| self.envs: List[Tuple[str, str]] = [] |
| |
| def input_options(self, env: str, msg: str, options: List[str]) -> str: |
| value = None |
| env_value = os.getenv(env) |
| if env_value is not None and env_value.lower() in options: |
| value = env_value |
| else: |
| options_pretty = [] + options |
| options_pretty[0] = f"{options[0].upper()}" |
| value = input(f"{msg} [{'/'.join(options_pretty)}] > ") |
| if value == "": |
| value = options[0] |
| while value.lower() not in options: |
| value = input(f"Invalid input {value.lower()}, please retry: ") |
| |
| value = value.lower() |
| self.envs.append((env, value)) |
| return value |
| |
| def input_default(self, env: str, msg: str, default: str) -> str: |
| value = None |
| env_value = os.getenv(env) |
| if env_value is not None: |
| value = env_value |
| else: |
| value = input(f"{msg} [{default}] > ") |
| if value == "": |
| value = default |
| |
| self.envs.append((env, value)) |
| return value |
| |
| def run(self): |
| from dependencies.tool import Tool |
| from dependencies.get_tag import NoGitException, get_tag |
| from dependencies.env_info import OSInfo |
| |
| try: |
| import venv |
| except ImportError: |
| print( |
| "Python venv does not appear to be installed, and is required for local installations.", |
| file=sys.stderr, |
| ) |
| |
| try: |
| ol_version = get_tag() |
| except NoGitException: |
| print( |
| "Installing OpenLane locally requires a Git repository.", |
| file=sys.stderr, |
| ) |
| exit(-1) |
| |
| tools = Tool.from_metadata_yaml(open("./dependencies/tool_metadata.yml").read()) |
| |
| print( |
| textwrap.dedent( |
| """\ |
| OpenLane Local Installer |
| |
| Copyright 2021-2022 Efabless Corporation. Available under the Apache License, |
| Version 2.0. |
| |
| Ctrl+C at any time to quit. |
| |
| Make sure you read the documentation in ./docs/source/local_installs.md. |
| """ |
| ) |
| ) |
| |
| install_dir = realpath("./install") |
| |
| sh("mkdir", "-p", install_dir, root="retry") |
| |
| home_perms = os.stat(os.getenv("HOME")) |
| sh( |
| "chown", |
| "-R", |
| "%i:%i" % (home_perms.st_uid, home_perms.st_gid), |
| install_dir, |
| root="retry", |
| ) |
| |
| os_list = ["other", "ubuntu-20.04", "centos-7", "arch", "macos"] |
| |
| # Try to determine user's OS |
| def set_default_os(x): |
| os_list.insert(0, os_list.pop(os_list.index(x))) |
| |
| os_info = OSInfo.get() |
| |
| if os_info.distro == "macOS": |
| set_default_os("macos") |
| |
| if os_info.distro == "centos" and os_info.distro_version == "7": |
| set_default_os("centos-7") |
| |
| if os_info.distro == "ubuntu" and os_info.distro_version == "20.04": |
| set_default_os("ubuntu-20.04") |
| |
| if os_info.distro in ["manjaro", "arch"]: |
| set_default_os("arch") |
| |
| os_pick = self.input_options( |
| "OS", "Which UNIX/Unix-like OS are you using?", os_list |
| ) |
| |
| gcc_bin = os.getenv("CC") or "gcc" |
| gxx_bin = os.getenv("CXX") or "g++" |
| try: |
| if os_pick not in [ |
| "centos-7", |
| "macos", |
| ]: # The reason we ignore centos 7 and macos is that we're going to just use devtoolset-8/brew gcc anyway. |
| all_output = "" |
| try: |
| gcc_ver_output = subprocess.run( |
| [gcc_bin, "--version"], stdout=subprocess.PIPE |
| ) |
| all_output += gcc_ver_output.stdout.decode("utf8") |
| gx_ver_output = subprocess.run( |
| [gxx_bin, "--version"], stdout=subprocess.PIPE |
| ) |
| all_output += gx_ver_output.stdout.decode("utf8") |
| except Exception: |
| pass |
| if "clang" in all_output: |
| print( |
| textwrap.dedent( |
| f"""\ |
| We've detected that you're using Clang as your default C or C++ compiler. |
| Unfortunately, Clang is not compatible with some of the tools being |
| installed. |
| |
| You may continue this installation at your own risk, but we recommend |
| installing GCC. |
| |
| You can specify a compiler to use explicitly by invoking this script as |
| follows, for example: |
| |
| CC=/usr/local/bin/gcc-8 CXX=/usr/local/bin/g++-8 python3 {__file__} |
| """ |
| ) |
| ) |
| input( |
| "Press return if you understand the risk and wish to continue anyways >" |
| ) |
| except FileNotFoundError as e: |
| print(e, "(set as either CC or CXX)") |
| exit(os.EX_CONFIG) |
| |
| install_packages = "no" |
| if os_pick != "other": |
| install_packages = self.input_options( |
| "INSTALL_PACKAGES", |
| "Do you want to install dependencies using your package manager?", |
| ["no", "yes"], |
| ) |
| if install_packages != "no": |
| |
| def cat_all(dir): |
| result = "" |
| for file in os.listdir(dir): |
| result += open(join(dir, file)).read() |
| result += "\n" |
| return result |
| |
| if os_pick == "macos": |
| brew_packages = ( |
| cat_all(join(openlane_dir, "dependencies", "macos")) |
| .strip() |
| .split("\n") |
| ) |
| |
| sh("brew", "install", *brew_packages) |
| if os_pick == "centos-7": |
| yum_packages = ( |
| cat_all(join(openlane_dir, "dependencies", "centos-7")) |
| .strip() |
| .split("\n") |
| ) |
| |
| sh("yum", "install", "-y", *yum_packages, root="retry") |
| if os_pick == "arch": |
| raw = ( |
| cat_all(join(openlane_dir, "dependencies", "arch")) |
| .strip() |
| .split("\n") |
| ) |
| |
| arch_packages = [] |
| aur_packages = [] |
| |
| for entry in raw: |
| if entry.strip() == "": |
| continue |
| |
| if entry.startswith("https://"): |
| aur_packages.append(entry) |
| else: |
| arch_packages.append(entry) |
| |
| sh( |
| "pacman", |
| "-S", |
| "--noconfirm", |
| "--needed", |
| *arch_packages, |
| root="retry", |
| ) |
| |
| temp_dir = tempfile.gettempdir() |
| oaur_path = os.path.join(temp_dir, "openlane_aur") |
| pathlib.Path(oaur_path).mkdir(parents=True, exist_ok=True) |
| with chdir(oaur_path): |
| for package in aur_packages: |
| sh("rm", "-rf", "current") |
| sh("git", "clone", package, "current") |
| with chdir("current"): |
| sh("makepkg", "-si", "--noconfirm") |
| if os_pick == "ubuntu-20.04": |
| raw = ( |
| cat_all(join(openlane_dir, "dependencies", "ubuntu-20.04")) |
| .strip() |
| .split("\n") |
| ) |
| |
| apt_packages = [] |
| apt_debs = [] |
| |
| for entry in raw: |
| if entry.strip() == "": |
| continue |
| |
| if entry.startswith("https://"): |
| apt_debs.append(entry) |
| else: |
| apt_packages.append(entry) |
| sh("apt-get", "update", root="retry") |
| sh("apt-get", "install", "-y", "curl", root="retry") |
| for deb in apt_debs: |
| path = download(deb, "deb") |
| sh("apt-get", "install", "-y", "-f", path, root="retry") |
| sh("apt-get", "install", "-y", *apt_packages, root="retry") |
| |
| print("To re-run with the same options: ") |
| print(f"{' '.join(['%s=%s' % env for env in self.envs])} python3 {__file__}") |
| |
| run_env = os.environ.copy() |
| run_env["PREFIX"] = install_dir |
| run_env["PATH"] = f"{install_dir}/bin:{os.getenv('PATH')}" |
| |
| path_elements = ["$OL_INSTALL_DIR/venv/bin", "$OL_INSTALL_DIR/bin"] |
| |
| if os_pick == "centos-7": |
| run_env["CC"] = "/opt/rh/devtoolset-8/root/usr/bin/gcc" |
| run_env["CXX"] = "/opt/rh/devtoolset-8/root/usr/bin/g++" |
| run_env["PATH"] = f"/opt/rh/devtoolset-8/root/usr/bin:{os.getenv('PATH')}" |
| run_env[ |
| "LD_LIBRARY_PATH" |
| ] = f"/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib:/opt/rh/devtoolset-8/root/usr/lib64/dyninst:/opt/rh/devtoolset-8/root/usr/lib/dyninst:/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib:{os.getenv('LD_LIBRARY_PATH')}" |
| run_env[ |
| "CMAKE_INCLUDE_PATH" |
| ] = f"/usr/include/boost169:{os.getenv('CMAKE_INCLUDE_PATH')}" |
| run_env[ |
| "CMAKE_LIBRARY_PATH" |
| ] = f"/lib64/boost169:{os.getenv('CMAKE_LIBRARY_PATH')}" |
| elif os_pick == "macos": |
| |
| def get_prefix(tool): |
| return ( |
| subprocess.check_output(["brew", "--prefix", tool]) |
| .decode("utf8") |
| .strip() |
| ) |
| |
| klayout_app_path = self.input_default( |
| "KLAYOUT_MAC_APP", |
| "Please input the path to klayout.app (0.27.3 or later): ", |
| "/Applications/klayout.app", |
| ) |
| klayout_path_element = join(klayout_app_path, "Contents", "MacOS") |
| |
| run_env["CC"] = f"{get_prefix('gcc')}/bin/gcc-11" |
| run_env["CXX"] = f"{get_prefix('gcc')}/bin/g++-11" |
| run_env[ |
| "PATH" |
| ] = f"{get_prefix('swig@3')}/bin:{get_prefix('bison')}/bin:{get_prefix('flex')}/bin:{get_prefix('gnu-which')}/bin:{os.getenv('PATH')}" |
| run_env[ |
| "MAGIC_CONFIG_OPTS" |
| ] = f"--with-tcl={get_prefix('tcl-tk')} --with-tk={get_prefix('tcl-tk')}" |
| run_env["READLINE_CXXFLAGS"] = f"CXXFLAGS=-L{get_prefix('readline')}/lib" |
| |
| path_elements.append(f"{klayout_path_element}") |
| path_elements.append(f"{get_prefix('gnu-sed')}/libexec/gnubin") |
| path_elements.append(f"{get_prefix('bash')}/bin") |
| else: |
| run_env["CC"] = gcc_bin |
| self.envs.append(("CC", gcc_bin)) |
| run_env["CXX"] = gxx_bin |
| self.envs.append(("CXX", gxx_bin)) |
| |
| def copy(f): |
| sh("rm", "-rf", f) |
| sh("cp", "-r", join(openlane_dir, f), f) |
| |
| def install(): |
| print("Copying files...") |
| for folder in ["bin", "lib", "share", "build", "dependencies"]: |
| sh("mkdir", "-p", folder) |
| |
| print("Building Python virtual environment...") |
| venv_builder = venv.EnvBuilder(clear=True, with_pip=True) |
| venv_builder.create("./venv") |
| |
| subprocess.run( |
| [ |
| "bash", |
| "-c", |
| """ |
| source ./venv/bin/activate |
| python3 -m pip install --upgrade -r ../dependencies/python/precompile_time.txt |
| python3 -m pip install --upgrade -r ../dependencies/python/compile_time.txt |
| python3 -m pip install --upgrade -r ../dependencies/python/run_time.txt |
| """, |
| ] |
| ) |
| |
| print("Installing dependencies...") |
| with chdir("build"): |
| for folder in ["repos", "versions"]: |
| sh("mkdir", "-p", folder) |
| |
| skip_tools = re.compile(os.getenv("SKIP_TOOLS") or "Unmatchable") |
| tool_queue = list(tools.values()).copy() |
| |
| print(tool_queue) |
| |
| def pop(): |
| return tool_queue.pop(0) if len(tool_queue) else None |
| |
| installed = set() |
| tool = pop() |
| while tool is not None: |
| if not (tool.in_install and (skip_tools.match(tool.name) is None)): |
| tool = pop() |
| continue |
| |
| if len(tool.dependencies): |
| dependencies = set(tool.dependencies) |
| if not dependencies.issubset(installed): |
| tool_queue.append(tool) |
| tool = pop() |
| continue |
| |
| installed_version = "" |
| version_path = f"versions/{tool.name}" |
| try: |
| installed_version = open(version_path).read() |
| except Exception: |
| pass |
| if ( |
| installed_version == tool.version_string |
| and os.getenv("FORCE_REINSTALL") != "1" |
| ): |
| print(f"{tool.version_string} already installed, skipping...") |
| else: |
| print(f"Installing {tool.name}...") |
| |
| with chdir("repos"): |
| if not exists(tool.name): |
| sh("git", "clone", tool.repo, tool.name) |
| |
| with chdir(tool.name): |
| sh("git", "fetch") |
| sh("git", "submodule", "update", "--init") |
| sh("git", "checkout", tool.commit) |
| subprocess.run( |
| [ |
| "bash", |
| "-c", |
| f"""\ |
| set -e |
| source {install_dir}/venv/bin/activate |
| {tool.build_script} |
| """, |
| ], |
| env=run_env, |
| check=True, |
| ) |
| |
| with open(version_path, "w") as f: |
| f.write(tool.version_string) |
| |
| installed.add(tool.name) |
| tool = pop() |
| |
| path_elements.reverse() |
| with open("env.tcl", "w") as f: |
| f.write( |
| textwrap.dedent( |
| f"""\ |
| set OL_INSTALL_DIR [file dirname [file normalize [info script]]] |
| |
| set ::env(OPENLANE_LOCAL_INSTALL) 1 |
| set ::env(OL_INSTALL_DIR) "$OL_INSTALL_DIR" |
| set ::env(PATH) "{":".join(path_elements)}:$::env(PATH)" |
| set ::env(VIRTUAL_ENV) "$OL_INSTALL_DIR/venv" |
| """ |
| ) |
| ) |
| |
| with open("installed_version", "w") as f: |
| f.write(ol_version) |
| |
| with chdir(install_dir): |
| install() |
| |
| print("Done.") |
| print( |
| "To invoke Openlane from now on, invoke ./flow.tcl from the OpenLane root without the Makefile." |
| ) |