From 5454a7dd14099b45ddab016693280e48832e54a8 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sun, 12 Jan 2025 14:37:06 +0000 Subject: [PATCH] Bonsai offers to install Git and ifcmerge If Git isn't installed on Windows, the Git panel now offers to install it from the Microsoft Store using `winget`. Then if ifcmerge isn't installed on Linux or Windows, the Git panel offers to install it from copies bundled in the Bonsai ZIP package. Note that ifcmerge.exe increases the size of the Bonsai ZIP download by about 7MB. --- src/bonsai/Makefile | 8 ++++ .../bonsai/bim/module/ifcgit/__init__.py | 2 + .../bonsai/bim/module/ifcgit/operator.py | 33 +++++++++++++ src/bonsai/bonsai/bim/module/ifcgit/ui.py | 16 +++++++ src/bonsai/bonsai/core/ifcgit.py | 17 +++++++ src/bonsai/bonsai/tool/ifcgit.py | 47 +++++++++++++++++++ 6 files changed, 123 insertions(+) diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index 86da37a8ac..b937a932d3 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -229,6 +229,14 @@ endif # Required for Desktop icon and file association cp -r bonsai/libs/desktop build/bonsai/libs/ + # required for three-way git merging +ifeq ($(PLATFORM), win) + cd build/bonsai/libs/desktop && wget https://github.com/brunopostle/ifcmerge/releases/download/2024-06-24/ifcmerge.zip + cd build/bonsai/libs/desktop && unzip ifcmerge.zip && rm ifcmerge.zip +else + cd build/bonsai/libs/desktop && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge +endif + # Generate translations module for Bonsai build git clone https://github.com/IfcOpenShell/bonsai-translations.git build/working $(PYTHON) scripts/bonsai_translations.py -i "build/working" -o "build/bonsai" diff --git a/src/bonsai/bonsai/bim/module/ifcgit/__init__.py b/src/bonsai/bonsai/bim/module/ifcgit/__init__.py index 27352bbc3e..6c87a659e8 100644 --- a/src/bonsai/bonsai/bim/module/ifcgit/__init__.py +++ b/src/bonsai/bonsai/bim/module/ifcgit/__init__.py @@ -37,6 +37,8 @@ classes = ( operator.Push, operator.RefreshGit, operator.SwitchRevision, + operator.InstallGit, + operator.InstallIfcmerge, prop.IfcGitTag, prop.IfcGitListItem, prop.IfcGitProperties, diff --git a/src/bonsai/bonsai/bim/module/ifcgit/operator.py b/src/bonsai/bonsai/bim/module/ifcgit/operator.py index b059c71b2d..4555a94190 100644 --- a/src/bonsai/bonsai/bim/module/ifcgit/operator.py +++ b/src/bonsai/bonsai/bim/module/ifcgit/operator.py @@ -386,3 +386,36 @@ class ObjectLog(bpy.types.Operator): step_id = context.active_object.BIMObjectProperties.ifc_definition_id core.entity_log(tool.IfcGit, tool.Ifc, step_id, self) return {"FINISHED"} + + +class InstallGit(bpy.types.Operator): + """Installs Git if possible""" + + bl_label = "Install Git" + bl_idname = "ifcgit.install_git" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + IfcGitData.make_sure_is_loaded() + if IfcGitData.data["git_exe"]: + return False + return True + + def execute(self, context): + core.install_git(tool.IfcGit, self) + refresh() + return {"FINISHED"} + + +class InstallIfcmerge(bpy.types.Operator): + """Installs ifcmerge if possible""" + + bl_label = "Install ifcmerge" + bl_idname = "ifcgit.install_ifcmerge" + bl_options = {"REGISTER"} + + def execute(self, context): + core.install_ifcmerge(tool.IfcGit, self) + refresh() + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/ifcgit/ui.py b/src/bonsai/bonsai/bim/module/ifcgit/ui.py index 860e4325d3..25665d362c 100644 --- a/src/bonsai/bonsai/bim/module/ifcgit/ui.py +++ b/src/bonsai/bonsai/bim/module/ifcgit/ui.py @@ -1,6 +1,7 @@ import bpy import time import os +import platform from bonsai.bim.module.ifcgit.data import IfcGitData @@ -26,7 +27,22 @@ class IFCGIT_PT_panel(bpy.types.Panel): if not IfcGitData.data["git_exe"]: row = layout.row() row.label(text="Git is not installed", icon="ERROR") + if platform.system() == "Windows": + row.operator( + "ifcgit.install_git", + text="Install Git from Microsoft Store", + icon="PACKAGE", + ) return + elif not IfcGitData.data["ifcmerge_exe"]: + # TODO check if ifcmerge is up-to-date + row = layout.row() + row.label(text="ifcmerge is not installed", icon="ERROR") + row.operator( + "ifcgit.install_ifcmerge", + text="Install ifcmerge", + icon="PACKAGE", + ) props = context.scene.IfcGitProperties diff --git a/src/bonsai/bonsai/core/ifcgit.py b/src/bonsai/bonsai/core/ifcgit.py index 17dde89fad..6e4ac90ac9 100644 --- a/src/bonsai/bonsai/core/ifcgit.py +++ b/src/bonsai/bonsai/core/ifcgit.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . from __future__ import annotations +import platform from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: @@ -130,3 +131,19 @@ def entity_log(ifcgit: tool.IfcGit, ifc: tool.Ifc, step_id: int, operator: bpy.t log_text = ifcgit.entity_log(path_ifc, step_id) # ERROR is only way to display a multi-line message operator.report({"ERROR"}, log_text) + + +def install_git(ifcgit: tool.IfcGit, operator: bpy.types.Operator) -> None: + if platform.system() == "Windows": + ifcgit.install_git_windows(operator=operator) + else: + print("install_git() not implemented") + + +def install_ifcmerge(ifcgit: tool.IfcGit, operator: bpy.types.Operator) -> None: + if platform.system() == "Windows": + ifcgit.install_ifcmerge_windows(name_exe="ifcmerge.exe", operator=operator) + elif platform.system() == "Linux": + ifcgit.install_ifcmerge_linux(name_exe="ifcmerge", operator=operator) + elif platform.system() == "Darwin": + print("install_ifcmerge() not implemented") diff --git a/src/bonsai/bonsai/tool/ifcgit.py b/src/bonsai/bonsai/tool/ifcgit.py index 0fcc9abc3b..011d5800a9 100644 --- a/src/bonsai/bonsai/tool/ifcgit.py +++ b/src/bonsai/bonsai/tool/ifcgit.py @@ -19,6 +19,8 @@ from __future__ import annotations import os import re +import subprocess +import shutil import bpy import logging from bonsai.bim import import_ifc @@ -546,6 +548,51 @@ class IfcGit: logtext = "No Git history found :(" return logtext + @classmethod + def install_git_windows(cls, operator: bpy.types.Operator) -> None: + """Command to install Git on Windows using winget""" + command = ["winget", "install", "--id", "Git.Git", "-e", "--source", "msstore"] + try: + subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as e: + operator.report({"ERROR"}, f"Called Process Error occurred: {e}") + except FileNotFoundError: + operator.report({"ERROR"}, "Winget is not available. Make sure Windows Package Manager is installed.") + + @classmethod + def install_ifcmerge_linux(cls, name_exe: str, operator: bpy.types.Operator) -> None: + """Command to install ifcmerge on Linux""" + src_dir = os.path.join(os.path.dirname(__file__), "../libs/desktop") + destdir = os.path.join(os.environ["HOME"], ".local", "bin") + try: + os.makedirs(destdir, exist_ok=True) + shutil.copy(os.path.join(src_dir, name_exe), destdir) + os.chmod(os.path.join(destdir, name_exe), 0o755) + except Exception as e: + operator.report({"ERROR"}, f"Error installing file: {e}") + + @classmethod + def install_ifcmerge_windows(cls, name_exe: str, operator: bpy.types.Operator) -> None: + """Command to install ifcmerge on Windows""" + src_dir = os.path.join(os.path.dirname(__file__), "..\\libs\\desktop") + destdir = os.path.join(os.environ["USERPROFILE"], "AppData", "Local", "Bonsai", "bin") + try: + os.makedirs(destdir, exist_ok=True) + shutil.copy(os.path.join(src_dir, name_exe), destdir) + except Exception as e: + operator.report({"ERROR"}, f"Error installing file: {e}") + + current_path = os.environ["PATH"] + + if destdir not in current_path: + os.environ["PATH"] = current_path + ";" + destdir + command = f'setx PATH "%PATH%;{destdir}"' + try: + subprocess.run(command, check=True, shell=True) + print(f"User %PATH% updated permanently with: {destdir}") + except subprocess.CalledProcessError as e: + operator.report({"ERROR"}, f"Error permanently updating %PATH%: {e}") + class IfcGitRepo: repo: git.Repo = None