* Use deps script to make life easier for contribs * Make readme more developer friendly * YAML file to describe Linux deps * Silent fail for yaml module (Python 3.6+) * Move Qt install to deps script * Update readme to add Windows command * Fixed Linux package name * Simplify env vars for workflow * Exclude scripts from SonarCloud * Move all deps config to .yml file * Fixed exclude glob * Simplify quickstart * Fixed macOS deps config key name * Fixed bug in Linux config read * Use backslash for consistency * Change SonarCloud path to /build/ * Fixed dep name * Exclude scripts in CMake coverage * Move script coverage exclusion to online config * Use try/except instead of checking return value * Fixed comment typo
336 lines
9 KiB
Python
336 lines
9 KiB
Python
import os
|
|
from lib import windows
|
|
import subprocess
|
|
import sys
|
|
import argparse
|
|
import traceback
|
|
|
|
config_file = "deps.yml"
|
|
|
|
|
|
class EnvError(Exception):
|
|
pass
|
|
|
|
|
|
class YamlError(Exception):
|
|
pass
|
|
|
|
|
|
class PlatformError(Exception):
|
|
pass
|
|
|
|
|
|
class PathError(Exception):
|
|
pass
|
|
|
|
|
|
try:
|
|
import yaml # type: ignore
|
|
except ImportError:
|
|
# this is fairly common in earlier versions of python3,
|
|
# which is normally what you find on mac and windows.
|
|
print("Python yaml module missing, please install: pip install pyyaml")
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
"""Entry point for the script."""
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--pause-on-exit", action="store_true", help="Useful on Windows"
|
|
)
|
|
parser.add_argument(
|
|
"--only", type=str, help="Only install the specified dependency"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
deps = Dependencies(args.only)
|
|
deps.install()
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
if args.pause_on_exit:
|
|
input("Press enter to continue...")
|
|
|
|
|
|
def run(command, check=True):
|
|
"""Runs a shell command and by default asserts that the return code is 0."""
|
|
|
|
command_str = command
|
|
if isinstance(command, list):
|
|
command_str = " ".join(command)
|
|
|
|
print(f"Running: {command_str}")
|
|
|
|
try:
|
|
subprocess.run(command, shell=True, check=check)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Command failed: {command_str}", file=sys.stderr)
|
|
raise e
|
|
|
|
|
|
def get_os():
|
|
"""Detects the operating system."""
|
|
if sys.platform == "win32":
|
|
return "windows"
|
|
elif sys.platform == "darwin":
|
|
return "mac"
|
|
elif sys.platform.startswith("linux"):
|
|
return "linux"
|
|
else:
|
|
raise PlatformError(f"Unsupported platform: {sys.platform}")
|
|
|
|
|
|
def get_linux_distro():
|
|
"""Detects the Linux distro."""
|
|
os_file = "/etc/os-release"
|
|
if os.path.isfile(os_file):
|
|
with open(os_file) as f:
|
|
for line in f:
|
|
if line.startswith("ID="):
|
|
return line.strip().split("=")[1].strip('"')
|
|
return None
|
|
|
|
|
|
class Config:
|
|
"""Reads the dependencies configuration file."""
|
|
|
|
def __init__(self):
|
|
with open(config_file, "r") as f:
|
|
data = yaml.safe_load(f)
|
|
|
|
os_name = get_os()
|
|
try:
|
|
root = data["dependencies"]
|
|
except KeyError:
|
|
raise YamlError(f"Nothing found in {config_file} for: dependencies")
|
|
|
|
try:
|
|
self.os = root[os_name]
|
|
except KeyError:
|
|
raise YamlError(f"Nothing found in {config_file} for: {os_name}")
|
|
|
|
def get_qt_config(self):
|
|
try:
|
|
return self.os["qt"]
|
|
except KeyError:
|
|
raise YamlError(f"Nothing found in {config_file} for: qt")
|
|
|
|
def get_packages_file(self):
|
|
try:
|
|
return self.os["packages"]
|
|
except KeyError:
|
|
raise YamlError(f"Nothing found in {config_file} for: packages")
|
|
|
|
def get_linux_package_command(self, distro):
|
|
try:
|
|
distro_data = self.os[distro]
|
|
except KeyError:
|
|
raise YamlError(f"Nothing found in {config_file} for: {distro}")
|
|
|
|
try:
|
|
command_base = distro_data["command"]
|
|
except KeyError:
|
|
raise YamlError(f"No package command found in {config_file} for: {distro}")
|
|
|
|
try:
|
|
package_data = distro_data["packages"]
|
|
except KeyError:
|
|
raise YamlError(f"No package list found in {config_file} for: {distro}")
|
|
|
|
packages = " ".join(package_data)
|
|
return f"{command_base} {packages}"
|
|
|
|
|
|
class Dependencies:
|
|
|
|
def __init__(self, only):
|
|
self.config = Config()
|
|
self.only = only
|
|
|
|
def install(self):
|
|
"""Installs dependencies for the current platform."""
|
|
|
|
os = get_os()
|
|
if os == "windows":
|
|
self.windows()
|
|
elif os == "mac":
|
|
self.mac()
|
|
elif os == "linux":
|
|
self.linux()
|
|
else:
|
|
raise PlatformError(f"Unsupported platform: {os}")
|
|
|
|
def windows(self):
|
|
"""Installs dependencies on Windows."""
|
|
|
|
if not windows.is_admin():
|
|
windows.relaunch_as_admin(__file__)
|
|
sys.exit()
|
|
|
|
ci_env = os.environ.get("CI")
|
|
if ci_env:
|
|
print("CI environment detected")
|
|
|
|
only_qt = self.only == "qt"
|
|
|
|
# for ci, skip qt; we install qt separately so we can cache it.
|
|
if not ci_env or only_qt:
|
|
qt = WindowsQt(self.config.get_qt_config())
|
|
qt_install_dir = qt.get_install_dir()
|
|
if qt_install_dir:
|
|
print(f"Skipping Qt, already installed at: {qt_install_dir}")
|
|
else:
|
|
qt.install()
|
|
|
|
if only_qt:
|
|
return
|
|
|
|
# use winget instead of choco to install the vc++ deps, since in choco there is no way
|
|
# to load a choco config file but skip a specific package (i.e. to skip vc++ for ci)
|
|
if not ci_env:
|
|
winget = WindowsWinGet()
|
|
winget.install_visual_studio()
|
|
|
|
choco = WindowsChoco()
|
|
if ci_env:
|
|
choco.config_ci_cache()
|
|
|
|
try:
|
|
command = self.config.os["command"]
|
|
except KeyError:
|
|
raise YamlError(f"Nothing found in {config_file} on Windows for: command")
|
|
|
|
choco.install(command, ci_env)
|
|
|
|
def mac(self):
|
|
"""Installs dependencies on macOS."""
|
|
try:
|
|
command = self.config.os["command"]
|
|
except KeyError:
|
|
raise YamlError(f"Nothing found in {config_file} on Mac for: command")
|
|
|
|
run(command)
|
|
|
|
def linux(self):
|
|
"""Installs dependencies on Linux."""
|
|
|
|
distro = get_linux_distro()
|
|
if not distro:
|
|
raise PlatformError("Unable to detect Linux distro")
|
|
|
|
command = self.config.get_linux_package_command(distro)
|
|
run(command)
|
|
|
|
|
|
class WindowsWinGet:
|
|
"""WinGet for Windows."""
|
|
|
|
def install_visual_studio(self):
|
|
"""Installs packages using WinGet."""
|
|
|
|
override = [
|
|
"--quiet",
|
|
"--wait",
|
|
"--includeRecommended",
|
|
"--add Microsoft.VisualStudio.Workload.MSBuildTools",
|
|
"--add Microsoft.VisualStudio.Workload.VCTools",
|
|
]
|
|
|
|
args = [
|
|
"winget",
|
|
"install",
|
|
"--silent",
|
|
"--id",
|
|
"Microsoft.VisualStudio.2022.BuildTools",
|
|
"--override",
|
|
f'"{" ".join(override)}"',
|
|
]
|
|
|
|
run(
|
|
" ".join(args),
|
|
check=False,
|
|
)
|
|
|
|
|
|
class WindowsChoco:
|
|
"""Chocolatey for Windows."""
|
|
|
|
def install(self, command, ci_env):
|
|
"""Installs packages using Chocolatey."""
|
|
if ci_env:
|
|
# don't show noisy choco progress bars in ci env
|
|
run(f"{command} --no-progress")
|
|
else:
|
|
run(command)
|
|
|
|
def config_ci_cache(self):
|
|
"""Configures Chocolatey cache for CI."""
|
|
|
|
runner_temp_key = "RUNNER_TEMP"
|
|
runner_temp = os.environ.get(runner_temp_key)
|
|
if runner_temp:
|
|
# sets the choco cache dir, which should match the dir in the ci cache action.
|
|
key_arg = '--name="cacheLocation"'
|
|
value_arg = f'--value="{runner_temp}/choco"'
|
|
run(["choco", "config", "set", key_arg, value_arg])
|
|
else:
|
|
print(f"Warning: CI environment variable {runner_temp_key} not set")
|
|
|
|
|
|
class WindowsQt:
|
|
"""Qt for Windows."""
|
|
|
|
def __init__(self, config):
|
|
self.config = config
|
|
|
|
self.version = os.environ.get("QT_VERSION")
|
|
if not self.version:
|
|
try:
|
|
default_version = config["version"]
|
|
except KeyError:
|
|
raise EnvError(f"Qt version not set in {config_file}")
|
|
|
|
print(f"QT_VERSION not set, using: {default_version}")
|
|
self.version = default_version
|
|
|
|
self.base_dir = os.environ.get("QT_BASE_DIR")
|
|
if not self.base_dir:
|
|
try:
|
|
default_base_dir = config["install-dir"]
|
|
except KeyError:
|
|
raise EnvError(f"Qt install-dir not set in {config_file}")
|
|
|
|
print(f"QT_BASE_DIR not set, using: {default_base_dir}")
|
|
self.base_dir = default_base_dir
|
|
|
|
self.install_dir = f"{self.base_dir}\\{self.version}"
|
|
|
|
def get_install_dir(self):
|
|
if os.path.isdir(self.install_dir):
|
|
return self.install_dir
|
|
|
|
def install(self):
|
|
"""Installs Qt on Windows."""
|
|
|
|
run(["pip", "install", "aqtinstall"])
|
|
|
|
try:
|
|
mirror_url = self.config["mirror"]
|
|
except KeyError:
|
|
raise EnvError(f"Qt mirror not set in {config_file}")
|
|
|
|
args = ["python", "-m", "aqt", "install-qt"]
|
|
args.extend(["--outputdir", self.base_dir])
|
|
args.extend(["--base", mirror_url])
|
|
args.extend(["windows", "desktop", self.version, "win64_msvc2019_64"])
|
|
run(args)
|
|
|
|
install_dir = self.get_install_dir()
|
|
if not install_dir:
|
|
raise EnvError(f"Qt not installed, path not found: {install_dir}")
|
|
|
|
|
|
main()
|