deskflow/scripts/install_deps.py
Nick Bolton d0a4c3d7a0
Use deps script to make life easier for contribs (#7351)
* 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
2024-06-14 15:32:47 +00:00

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()