blob: c8f64eec2beafd70ce98c4ba109e0775ed81f592 [file] [log] [blame]
#!/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."
)