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