blob: a254828712639732869d80a473482289bb9cafa0 [file]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 os
import pathlib
import platform
import subprocess
import sys
from enum import Enum
from distro import distro
class VsWhereLocation(Enum):
CHOCO = 1
DEFAULT = 2
def _query_yes_no(question: str, no_confirm: bool) -> bool:
valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
if no_confirm:
print(f"Running {question} with noconfirm")
return True
while True:
print(f"{question} [y/n]", end=" ", flush=True)
choice = input().lower()
if choice in valid:
return valid[choice]
else:
print("Please respond with 'yes' or 'no' (or 'y' or 'n').")
def _run_command_with_confirm(command: str, no_confirm: bool) -> bool:
if _query_yes_no(f"Running {command}", no_confirm):
return os.system(command) == 0
class PackageManager:
def __init__(self, no_confirm):
self.no_confirm = no_confirm
def install(self, dependencies: dict[str, set[str]]) -> bool:
raise RuntimeError("NotImplementedException")
def install_compiler(self) -> str:
raise RuntimeError("NotImplementedException")
def ensure_environment(self):
pass
def _install(
self,
dependencies: dict[str, set[str]],
replace_dict: dict[str, set[str]],
install_cmd: str,
) -> bool:
dependencies.update(
{k: v for k, v in replace_dict.items() if k in dependencies}
)
dependencies = self._filter_out_installed_packages(dependencies)
dependencies_str = " ".join(
str(value) for value_set in dependencies.values() for value in value_set
)
if not dependencies_str or dependencies_str.isspace():
return True
return _run_command_with_confirm(
f"{install_cmd} {dependencies_str}", self.no_confirm
)
def _get_installed_packages(self) -> set[str]:
raise RuntimeError("NotImplementedException")
def _filter_out_installed_packages(self, dependencies: dict[str, set[str]]):
installed_packages = self._get_installed_packages()
filtered_packages = {
k: (v - installed_packages) for k, v in dependencies.items()
}
for installed_package in installed_packages:
filtered_packages.pop(installed_package, None)
return filtered_packages
def run_cmd(self, cmd: str) -> bool:
result = subprocess.run(f"{cmd}", shell=True, text=True, check=False)
return result.returncode == 0
class BrewPackageManager(PackageManager):
def __init__(self, no_confirm):
PackageManager.__init__(self, no_confirm)
def install(self, dependencies: dict[str, set[str]]) -> bool:
return self._install(
dependencies=dependencies,
install_cmd="brew install",
replace_dict={"patch": set()},
)
def install_compiler(self) -> str:
self.install({"compiler": set()})
return ""
def _get_installed_packages(self) -> set[str]:
result = subprocess.run(
["brew", "list"], text=True, capture_output=True, check=True
)
lines = result.stdout.splitlines()
lines = [line.split("@", 1)[0] for line in lines]
return set(lines)
def run_cmd(self, cmd: str) -> bool:
add_m4_to_path_cmd = 'export PATH="$(brew --prefix m4)/bin:$PATH"'
result = subprocess.run(
f"{add_m4_to_path_cmd} && {cmd}", shell=True, text=True, check=False
)
return result.returncode == 0
class AptPackageManager(PackageManager):
def __init__(self, no_confirm):
PackageManager.__init__(self, no_confirm)
def install(self, dependencies: dict[str, set[str]]) -> bool:
return self._install(
dependencies=dependencies,
install_cmd="sudo apt install -y",
replace_dict={"libarchive": {"liblzma-dev"}, "python": {"libpython3-dev"}},
)
def _get_installed_packages(self) -> set[str]:
result = subprocess.run(
["dpkg", "--get-selections"], text=True, capture_output=True, check=True
)
lines = [line.split("\t")[0] for line in result.stdout.splitlines()]
lines = [line.rsplit(":", 1)[0] for line in lines]
return set(lines)
def install_compiler(self) -> str:
self.install({"compiler": {"g++"}})
return ""
class DnfPackageManager(PackageManager):
def __init__(self, no_confirm, needs_epel):
PackageManager.__init__(self, no_confirm)
self.needs_epel = needs_epel
def install(self, dependencies: dict[str, set[str]]) -> bool:
if self.needs_epel:
install_cmd = "sudo dnf --enablerepo=crb install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm"
else:
install_cmd = "sudo dnf install -y"
return self._install(
dependencies=dependencies,
install_cmd=install_cmd,
replace_dict={"python": {"python3-devel"}},
)
def _get_installed_packages(self) -> set[str]:
result = subprocess.run(
["dnf", "list", "installed"], text=True, capture_output=True, check=True
)
lines = [line.split(" ")[0] for line in result.stdout.splitlines()]
lines = [line.rsplit(".", 1)[0] for line in lines]
return set(lines)
def install_compiler(self) -> str:
self.install({"compiler": {"gcc-c++"}})
return ""
class PacmanPackageManager(PackageManager):
def __init__(self, no_confirm):
PackageManager.__init__(self, no_confirm)
def install(self, dependencies: dict[str, set[str]]) -> bool:
return self._install(
dependencies=dependencies,
install_cmd="sudo pacman --noconfirm -S",
replace_dict={},
)
def _get_installed_packages(self) -> set[str]:
result = subprocess.run(
["pacman", "-Qq"], text=True, capture_output=True, check=True
)
return set(result.stdout.splitlines())
def install_compiler(self) -> str:
self.install({"compiler": {"gcc"}})
return ""
class ZypperPackageManager(PackageManager):
def __init__(self, no_confirm):
PackageManager.__init__(self, no_confirm)
def install(self, dependencies: dict[str, set[str]]) -> bool:
return self._install(
dependencies=dependencies,
install_cmd="sudo zypper install -y",
replace_dict={
"libarchive": {"libarchive-devel"},
"python": {"python3-devel"},
},
)
def _get_installed_packages(self) -> set[str]:
result = subprocess.run(
["zypper", "se", "--installed-only"],
text=True,
capture_output=True,
check=True,
)
lines = result.stdout.splitlines()
packages = set()
for line in lines:
if line.startswith(("S |", "--", " ")) or not line:
continue
parts = line.split("|")
if len(parts) > 2:
package_name = parts[1].strip()
packages.add(package_name)
return packages
def install_compiler(self) -> str:
self.install({"compiler": {"gcc-c++"}})
return ""
def _get_vs_dev_cmd_path(vs_where_location: VsWhereLocation):
if vs_where_location == VsWhereLocation.CHOCO:
vs_where_path = "vswhere"
else:
vs_where_path = (
"%ProgramFiles(x86)%\\Microsoft Visual Studio\\Installer\\vswhere.exe"
)
vswhere_results = subprocess.run(
f"{vs_where_path} -products * "
f"-property installationPath "
f"-requires Microsoft.VisualStudio.Component.VC.ATL "
f"-version 17",
capture_output=True,
check=False,
)
for vswhere_result in vswhere_results.stdout.splitlines():
possible_path = f"{vswhere_result.decode()}\\Common7\\Tools\\VsDevCmd.bat"
if os.path.exists(possible_path):
return f'"{possible_path}"'
return None
def _get_vs_dev_cmd(vs_where_location: VsWhereLocation) -> str:
vs_dev_path = _get_vs_dev_cmd_path(vs_where_location)
return f"{vs_dev_path} -arch=x64 -host_arch=x64"
def _get_activate_venv_path():
return pathlib.Path(__file__).parent.resolve() / "venv" / "Scripts" / "activate.bat"
def _minifi_setup_env_str(vs_where_location: VsWhereLocation) -> str:
return f"""
call refreshenv
call "{_get_activate_venv_path()}"
call {_get_vs_dev_cmd(vs_where_location)}
setlocal EnableDelayedExpansion
set PATH=!PATH:C:\\Strawberry\\c\\bin;=!;C:\\Program Files\\NASM;
endlocal & set PATH=%PATH%
set build_platform=x64
"""
def _create_minifi_setup_env_batch(vs_where_location: VsWhereLocation):
with open(
pathlib.Path(__file__).parent.resolve() / "build_environment.bat", "w"
) as f:
f.write(_minifi_setup_env_str(vs_where_location))
class ChocolateyPackageManager(PackageManager):
def __init__(self, no_confirm):
PackageManager.__init__(self, no_confirm)
def install(self, dependencies: dict[str, set[str]]) -> bool:
self._install(
dependencies=dependencies,
install_cmd="choco install -y",
replace_dict={
"python": set(),
"patch": set(),
"bison": set(),
"flex": set(),
"libarchive": set(),
"automake": set(),
"autoconf": set(),
"libtool": set(),
"make": set(),
"m4": {"gnuwin32-m4"},
"perl": {"strawberryperl", "NASM"},
},
)
return True
def _get_installed_packages(self) -> set[str]:
result = subprocess.run(
["choco", "list"], text=True, capture_output=True, check=True
)
lines = [line.split(" ")[0] for line in result.stdout.splitlines()]
lines = [line.rsplit(".", 1)[0] for line in lines]
if os.path.exists("C:\\Program Files\\NASM"):
lines.append("NASM") # choco doesnt remember NASM
return set(lines)
def _acquire_vswhere(self):
installed_packages = self._get_installed_packages()
if "vswhere" in installed_packages:
return VsWhereLocation.CHOCO
vswhere_default_path = (
"%ProgramFiles(x86)%\\Microsoft Visual Studio\\Installer\\vswhere.exe"
)
if os.path.exists(vswhere_default_path):
return VsWhereLocation.DEFAULT
self.install({"vswhere": {"vswhere"}})
return VsWhereLocation.CHOCO
def install_compiler(self) -> str:
vs_where_loc = self._acquire_vswhere()
vs_dev_path = _get_vs_dev_cmd_path(vs_where_loc)
if not vs_dev_path:
self.install(
{
"visualstudio2022buildtools": {
(
'visualstudio2022buildtools --package-parameters "--wait --quiet '
"--add Microsoft.VisualStudio.Workload.VCTools "
"--add Microsoft.VisualStudio.Component.VC.ATL "
'--includeRecommended"'
)
}
}
)
return ""
def run_cmd(self, cmd: str) -> bool:
env_bat_path = pathlib.Path(__file__).parent.resolve() / "build_environment.bat"
res = subprocess.run(f'"{env_bat_path}" & {cmd}', check=True, text=True)
return res.returncode == 0
def ensure_environment(self):
_create_minifi_setup_env_batch(self._acquire_vswhere())
def get_package_manager(no_confirm: bool) -> PackageManager:
platform_system = platform.system()
if platform_system == "Darwin":
return BrewPackageManager(no_confirm)
elif platform_system == "Linux":
distro_id = distro.id()
if distro_id == "ubuntu" or "debian" in distro_id:
return AptPackageManager(no_confirm)
elif "arch" in distro_id or "manjaro" in distro_id:
return PacmanPackageManager(no_confirm)
elif "rocky" in distro_id:
return DnfPackageManager(no_confirm, True)
elif "fedora" in distro_id:
return DnfPackageManager(no_confirm, False)
elif "opensuse" in distro_id:
return ZypperPackageManager(no_confirm)
else:
sys.exit(f"Unsupported platform {distro_id} exiting")
elif platform_system == "Windows":
return ChocolateyPackageManager(no_confirm)
else:
sys.exit(f"Unsupported platform {platform_system} exiting")