# This code is part of KQCircuits
# Copyright (C) 2024 IQM Finland Oy
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see
# https://www.gnu.org/licenses/gpl-3.0.html.
#
# The software distribution should follow IQM trademark policy for open-source software
# (meetiqm.com/iqm-open-source-trademark-policy). IQM welcomes contributions to the code.
# Please see our contribution agreements for individuals (meetiqm.com/iqm-individual-contributor-license-agreement)
# and organizations (meetiqm.com/iqm-organization-contributor-license-agreement).
from importlib import import_module
from io import IOBase
from pathlib import Path
from sys import platform
import os
import site
import setuptools
[docs]
def install_kqc_gui_dependencies():
"""Check KQCircuits' dependencies against klayout-requirements.txt file and install/upgrade if missing.
This is *only* for KLayout GUI. Stand-alone mode needs manual pip install or pip-sync, preferably in a venv.
This function should run only once at KLayout startup.
"""
# pylint: disable=import-outside-toplevel
from kqcircuits.pya_resolver import pya
# Skip installation in stand-alone python package mode
if not hasattr(pya, "MessageBox"):
return
detected_os = None
if os.name == "nt": # Windows
detected_os = "win"
elif os.name == "posix":
if platform == "darwin": # macOS
detected_os = "mac"
else:
detected_os = "linux"
else:
raise SystemError("Unsupported operating system.")
target_dir = os.path.split(setuptools.__path__[0])[0]
test_file = IOBase()
try:
test_file_path = os.path.join(target_dir, ".test.file")
# Following throws PermissionError if target_dir needs sudo
test_file = open(test_file_path, "x", encoding="utf-8") # pylint: disable=R1732
test_file.close()
if os.path.exists(test_file_path):
os.remove(test_file_path)
except PermissionError:
target_dir = site.USER_SITE
finally:
test_file.close()
mismatch = {}
# Check path expected after Developer guide installation
requirements_dir = Path(f"{os.path.dirname(__file__)}/../../kqcircuits_requirements")
if not requirements_dir.exists():
# Check path expected after SALT installation
requirements_dir = Path(f"{os.path.dirname(__file__)}/../../requirements")
if not requirements_dir.exists():
raise FileNotFoundError(
"Can't find gui-requirements.txt file. "
+ "If you used a developer GUI setup, try running 'python3 setup_within_klayout.py'"
)
requirements_file = f"{requirements_dir}/{detected_os}/gui-requirements.txt"
with open(requirements_file, encoding="utf-8") as f:
for line in f:
line = line.split("#")[0]
tokens = line.split("==")
if len(tokens) < 2:
continue
package = tokens[0].strip()
version = tokens[1].split("\\")[0].strip()
try:
mod = import_module(package)
if not hasattr(mod, "__version__"):
raise ModuleNotFoundError()
if mod.__version__ != version:
mismatch[package] = (version, mod.__version__)
except ModuleNotFoundError:
mismatch[package] = (version, None)
if not mismatch:
return
mismatch_msg = "\n".join(
[
(
f" '{package}' ({expected_version}) - not installed"
if not installed_version
else f" '{package}' expected version {expected_version}, got {installed_version}"
)
for package, (expected_version, installed_version) in mismatch.items()
]
)
# Install missing modules inside KLayout.
from pip import __main__
if hasattr(__main__, "_main"):
main = __main__._main
else:
from pip._internal.cli.main import main
ask = pya.MessageBox.warning(
"Dependencies out of date",
"Some dependencies for KQCircuits GUI were found to be out of date, according to\n"
+ f"{requirements_file}\n\n"
+ "The affected dependencies are:\n\n"
+ mismatch_msg
+ "\n\nInstall up to date dependencies?",
pya.MessageBox.Yes + pya.MessageBox.No,
)
if ask == pya.MessageBox.Yes:
main(["install", "-r", requirements_file, "--upgrade", f"--target={target_dir}"])
error_msg = ""
for package, (expected_version, _) in mismatch.items():
try:
mod = import_module(package)
if not hasattr(mod, "__version__"):
raise ModuleNotFoundError()
if mod.__version__ != expected_version:
error_msg += f"{package} still has version {mod.__version__} instead of {expected_version}\n"
except ModuleNotFoundError:
error_msg += f"{package} still not installed\n"
if error_msg:
pya.MessageBox.warning(
"Dependency update not in effect",
f"{error_msg}\nDependency update has not come into effect, KLayout restart is needed.\n\n"
+ "If a prompt related to dependencies appears again, "
+ "this means something went wrong with the update.\n",
0,
)