diff --git a/.gitignore b/.gitignore index 315ab6f..31fd78b 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,6 @@ ENV/ # mypy .mypy_cache/ + +#Don't add mac's dsstore +*.DS_Store* diff --git a/examples/MacCalc/.integrity.json b/examples/MacCalc/.integrity.json new file mode 100644 index 0000000..d24bf3b --- /dev/null +++ b/examples/MacCalc/.integrity.json @@ -0,0 +1 @@ +{"maccalc": "bdee4331072ce68954b7aa6d090165bc638474bf9515114a72d5baf86fa415af"} \ No newline at end of file diff --git a/examples/MacCalc/maccalc b/examples/MacCalc/maccalc new file mode 100644 index 0000000..ffd64d8 --- /dev/null +++ b/examples/MacCalc/maccalc @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +open -a calculator && cat \ No newline at end of file diff --git a/examples/old/test_github.py b/examples/old/test_github.py index 9297a8d..5b2d0c8 100644 --- a/examples/old/test_github.py +++ b/examples/old/test_github.py @@ -2,19 +2,31 @@ import iquail -if not iquail.helper.OS_WINDOWS: +if iquail.helper.OS_LINUX: raise AssertionError("This test solution is windows only") - -iquail.run( - solution=iquail.SolutionGitHub("cmder_mini.zip", "https://github.com/cmderdev/cmder"), - installer=iquail.Installer( - publisher='cmderdev', - name='Cmder', - icon='Cmder.exe', - binary='Cmder.exe', - console=False - ), - builder=iquail.builder.Builder( - iquail.builder.CmdIcon('icon.ico') +if iquail.helper.OS_WINDOWS: + iquail.run( + solution=iquail.SolutionGitHub("cmder_mini.zip", "https://github.com/cmderdev/cmder"), + installer=iquail.Installer( + publisher='cmderdev', + name='Cmder', + icon='Cmder.exe', + binary='Cmder.exe', + console=False + ), + builder=iquail.builder.Builder( + iquail.builder.CmdIcon('icon.ico') + )) +if iquail.helper.OS_OSX: + iquail.run( + solution=iquail.SolutionGitHub("Console.app.zip", "https://github.com/macmade/Console"), + installer=iquail.Installer( + name='Console', + publisher='MacMade', + full_app=True, + binary='Console.app', + icon='', + console=True + ), + builder=iquail.builder.Builder() ) -) diff --git a/examples/old/test_local.py b/examples/old/test_local.py index c712504..24e35e2 100644 --- a/examples/old/test_local.py +++ b/examples/old/test_local.py @@ -25,6 +25,19 @@ iquail.builder.CmdIntegrity(solution_path) ) ) +if iquail.helper.OS_OSX: + solution_path = ['Allum1'] + iquail.run( + iquail.SolutionLocal(['Allum1']), + iquail.Installer( + name='Allum1', + publisher='alies', + icon='icon.jpeg', + binary='allum1', + console=True, + ), + iquail.builder.Builder(iquail.builder.CmdIntegrity(solution_path)) + ) if iquail.helper.OS_WINDOWS: solution_path = ['OpenHardwareMonitor'] iquail.run( diff --git a/examples/test_osx.py b/examples/test_osx.py new file mode 100644 index 0000000..0d1e18b --- /dev/null +++ b/examples/test_osx.py @@ -0,0 +1,17 @@ +#!/usr/bin/python3 + +import iquail + +if iquail.helper.OS_OSX: + solution_path = ['MacCalc'] + iquail.run( + iquail.SolutionLocal(['MacCalc']), + iquail.Installer( + name='MacCalc', + publisher='test', + icon='icon.icns', + binary='maccalc', + console=True, + ), + iquail.builder.Builder(iquail.builder.CmdIntegrity(solution_path)) + ) diff --git a/examples/vagrant_gui.py b/examples/vagrant_gui.py new file mode 100644 index 0000000..b35071e --- /dev/null +++ b/examples/vagrant_gui.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 +import os +import iquail +import logging + + +logging.basicConfig( + level=logging.DEBUG, + filename=os.path.join(os.path.dirname(__file__), '..', 'iquail.log'), +) + +if not iquail.helper.OS_OSX: + raise AssertionError("This test solution is macOs only") + + +class FrameSelectMiniOrFull(iquail.controller_tkinter.FrameBaseConfigure): + def __init__(self, parent, controller): + super().__init__(parent, controller) + self.version_selected = self.add_combobox("Which version would you like to install?", + ('2.2.6', '2.2.6')) + + def next_pressed(self): + print(self.version_selected.get()) + version = self.version_selected.get().lower() + zip = "cmder_mini.zip" if version == "mini" else "cmder.zip" + self.manager.config.set("zip_url", zip) + self.controller.switch_to_install_frame() + +iquail.run( + solution=iquail.SolutionLocal(['Vagrant']), + installer=iquail.Installer( + publisher='HashiCorp', + name='vagrant', + icon='vagrant.pkg', + binary='vagrant.pkg', + console=False, + launch_with_quail=True, + ), + builder=iquail.builder.Builder( + iquail.builder.CmdIcon('icon.ico'), + iquail.builder.CmdNoconsole() + ), + controller=iquail.ControllerTkinter( + install_custom_frame=FrameSelectMiniOrFull), + conf_ignore=["config/*"] +) diff --git a/iquail/controller/__init__.py b/iquail/controller/__init__.py index 22e2015..808ce69 100644 --- a/iquail/controller/__init__.py +++ b/iquail/controller/__init__.py @@ -1,3 +1,8 @@ +import sys + from .controller_base import ControllerBase -from .controller_tkinter import ControllerTkinter +try: + from .controller_tkinter import ControllerTkinter +except ImportError as e: + print(f"Tkinter not found....", file=sys.stderr) from .controller_console import ControllerConsole diff --git a/iquail/controller/controller_console.py b/iquail/controller/controller_console.py index cd23aff..aaa974b 100644 --- a/iquail/controller/controller_console.py +++ b/iquail/controller/controller_console.py @@ -74,7 +74,7 @@ def start_uninstall(self): self.manager.uninstall() print("[*] %s successfully removed!" % self.manager.get_name()) self.press_to_exit() - except: + except Exception as e: print( "[*] Unknown error while uninstalling %s" % self.manager.get_name()) self.press_to_exit() diff --git a/iquail/helper/__init__.py b/iquail/helper/__init__.py index 80e62ed..9a24852 100644 --- a/iquail/helper/__init__.py +++ b/iquail/helper/__init__.py @@ -3,3 +3,5 @@ from .integrity_verifier import IntegrityVerifier, checksum_file from .configuration import Configuration, ConfVar from .linux_polkit_file import polkit_install, polkit_check +if OS_OSX: + from .osx import BundleTemplate, PlistCreator diff --git a/iquail/helper/misc.py b/iquail/helper/misc.py index bf7948a..ee6a12c 100644 --- a/iquail/helper/misc.py +++ b/iquail/helper/misc.py @@ -15,6 +15,7 @@ OS_LINUX = platform.system() == 'Linux' OS_WINDOWS = platform.system() == 'Windows' +OS_OSX = platform.system() == 'Darwin' _OVERRIDE_TMP_DIR = None diff --git a/iquail/helper/osx/__init__.py b/iquail/helper/osx/__init__.py new file mode 100644 index 0000000..8131754 --- /dev/null +++ b/iquail/helper/osx/__init__.py @@ -0,0 +1,2 @@ +from .bundle_template import BundleTemplate +from .plist_creator import PlistCreator diff --git a/iquail/helper/osx/bundle_template.py b/iquail/helper/osx/bundle_template.py new file mode 100644 index 0000000..25bc43d --- /dev/null +++ b/iquail/helper/osx/bundle_template.py @@ -0,0 +1,24 @@ +import os +import shutil + + +class BundleTemplate: + def __init__(self, bundle_name: str, base_dir='/Applications'): + self.full_path = os.path.join(base_dir, bundle_name + ".app") + self.names = [ + self.full_path, + os.path.join(self.full_path, 'Contents'), + os.path.join(self.full_path, 'Contents/MacOS'), + os.path.join(self.full_path, 'Contents/Resources') + ] + def installIcon(self, icon_file_name, icon_quail_path): + dest_path = os.path.join(self.full_path, 'Contents/Resources', icon_file_name) + try: + shutil.copy(icon_quail_path, dest_path) + except FileNotFoundError as e: + print("Error icon file {} was not found when installing".format(icon_quail_path)) + + + def make(self): + for path in self.names: + os.mkdir(path) diff --git a/iquail/helper/osx/plist_creator.py b/iquail/helper/osx/plist_creator.py new file mode 100644 index 0000000..c2d6bc2 --- /dev/null +++ b/iquail/helper/osx/plist_creator.py @@ -0,0 +1,78 @@ +import os +import xml.etree.ElementTree as ET + + +class PlistCreator: + def __init__(self, bundle_name: str, application_path: str, plist_dict=None): + # The argument plist_dict can't be mutable + if not plist_dict: + plist_dict = {} + self.__filename = os.path.join(application_path, bundle_name + ".app/Contents/Info.plist") + self.__data = "" + self.__plist_dict = { + "CFBundleGetInfoString": bundle_name, + "CFBundleExecutable": 'launcher', + "CFBundleIdentifier": bundle_name, + "CFBundleName": bundle_name + } + self.__plist_header = """ + + + """ + self.__plist_dict.update(plist_dict) + + @property + def plist_dict(self): + return self.__plist_dict + + def __make_key_with_text(self, key: str, value: str): + item = ET.Element(key) + item.text = value + return item + + def __indent_tree(self, elem, level=0): + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.__indent_tree(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + def __add_icon(self, root: ET.Element, icon_name: str): + root.append(self.__make_key_with_text('key', 'CFBundleIconFile')) + root.append(self.__make_key_with_text('string', icon_name)) + + def __create_tree(self) -> ET.Element: + root = ET.Element('plist', {'version':'1.0'}) + return root + + def build_tree_and_write_file(self): + tree = self.__create_tree() + + self.__data += self.__plist_header + self.__add_dict_content_to_tree(tree) + self.__indent_tree(tree) + self.__write_to_file(tree) + + def __add_dict_content_to_tree(self, root: ET.Element): + elem = ET.Element('dict') + for key, value in self.plist_dict.items(): + elem.append(self.__make_key_with_text('key', key)) + elem.append(self.__make_key_with_text('string', value)) + root.append(elem) + + def __write_to_file(self, tree): + tree_data = ET.tostring(tree, encoding='unicode') + with open(self.__filename, 'w+') as f: + total_data = self.__data + tree_data + f.write(total_data) + + + diff --git a/iquail/installer/installer.py b/iquail/installer/installer.py index 69f0877..00775b3 100644 --- a/iquail/installer/installer.py +++ b/iquail/installer/installer.py @@ -8,4 +8,6 @@ from .installer_windows import InstallerWindows Installer = InstallerWindows else: - raise NotImplementedError + from .installer_osx import InstallerOsx + Installer = InstallerOsx + diff --git a/iquail/installer/installer_osx.py b/iquail/installer/installer_osx.py new file mode 100644 index 0000000..5114dfe --- /dev/null +++ b/iquail/installer/installer_osx.py @@ -0,0 +1,78 @@ +from .installer_base import InstallerBase +import os +import stat +import shutil +import pathlib +from ..helper import BundleTemplate, PlistCreator + +class InstallerOsx(InstallerBase): + + def __init__(self, full_app, *args, **kwargs): + super().__init__(*args, **kwargs) + self._bundle_install_path = os.path.join(self._get_application_folder_path(), self.name + '.app') + self.is_full_app = full_app + + """ TODO: Add the icon to the bundle""" + def _register(self): + if (self._should_register_as_pkg()): + self._register_as_pkg() + return + if self.is_full_app: + shutil.copytree(self.binary, os.path.join(self._get_application_folder_path(), self._binary_name)) + launcher_path = os.path.join(self._bundle_install_path, 'Contents', 'MacOS', self.name) + st = os.stat(launcher_path) + os.chmod(launcher_path, st.st_mode | stat.S_IEXEC) + return + else: + bundle = BundleTemplate(self.name, base_dir=self._get_application_folder_path()) + icon_quail_path = self.get_solution_icon() + bundle.make() + if self._icon: + bundle.installIcon(self._icon, icon_quail_path) + plist = PlistCreator(self.name, self._get_application_folder_path(), {'CFBundleIconFile': self._icon}) + plist.build_tree_and_write_file() + self._build_launcher() + + def _unregister(self): + if (self._should_register_as_pkg()): + #self._register_as_pkg() + return + shutil.rmtree(self._bundle_install_path) + + def _registered(self): + if not os.path.exists(self.build_folder_path(self.binary)): + return False + return True + + def __add_to_path(self, binary, name): + os.symlink(binary, self.build_folder_path(name)) + + def _get_application_folder_path(self): + if self.install_systemwide: + return os.path.join(os.sep, 'Applications') + return os.path.join(str(pathlib.Path.home()), 'Applications') + + def _should_register_as_pkg(self): + if (self.binary.lower().endswith(".pkg")): + return True + return False + + def _register_as_pkg(self): + path = os.path.join(self._solution_path, self.binary) + pkg_installation = "installer -pkg " + path + " -target /" + print(path) + print(pkg_installation) + os.system("/usr/bin/osascript -e 'do shell script \"" + pkg_installation + "\" with administrator privileges'") + + def build_folder_path(self, name): + final_folder = os.path.join(self._get_application_folder_path(), self._name + '.app', 'Contents', 'MacOS') + return os.path.join(final_folder, name) + + def _build_launcher(self): + with open(os.path.join(self._bundle_install_path, 'Contents', 'MacOS', 'launcher'), 'w') as f: + shebang = '#!/usr/bin/env bash\n' + content = '/usr/bin/env python3 ' + self.launcher_binary + f.write(shebang) + f.write(content) + st = os.stat(os.path.join(self._bundle_install_path, 'Contents', 'MacOS', 'launcher')) + os.chmod(os.path.join(self._bundle_install_path, 'Contents', 'MacOS', 'launcher'), st.st_mode | stat.S_IEXEC) diff --git a/tests/base_test_osx_bundle.py b/tests/base_test_osx_bundle.py new file mode 100644 index 0000000..8bd4be2 --- /dev/null +++ b/tests/base_test_osx_bundle.py @@ -0,0 +1,20 @@ +import iquail.helper + +if iquail.helper.OS_OSX: + import shutil + + from tests.base_test_case import BaseTestCase + from iquail.helper.osx.bundle_template import BundleTemplate + from iquail.helper.osx.plist_creator import PlistCreator + + + class BaseTestOsxBundle(BaseTestCase): + # The test folder will always be named test.app + def setUp(self): + super(BaseTestOsxBundle, self).setUp() + self._test_folder_path = self.tmp('test.app') + self.bt = BundleTemplate('test', self._tmpdir) + self.bt.make() + + def tearDown(self): + shutil.rmtree(self.bt.full_path, ignore_errors=True) diff --git a/tests/test_bundle_osx.py b/tests/test_bundle_osx.py new file mode 100644 index 0000000..a775ca5 --- /dev/null +++ b/tests/test_bundle_osx.py @@ -0,0 +1,22 @@ +import iquail.helper + +if iquail.helper.OS_OSX: + import os + + from iquail.helper.osx.plist_creator import PlistCreator + from tests.base_test_osx_bundle import BaseTestOsxBundle + + + class TestOsxBundle(BaseTestOsxBundle): + """ + In this test we make sure that the proper folder has the right name and that all the subfolders are created correctly + """ + def test_default_bundle_setup(self): + # Bundle template create a folder appended with .app + assert os.path.exists(self._test_folder_path) + assert os.path.exists(os.path.join(self._test_folder_path, 'Contents')) + + def test_plist_creation(self): + plist_creator = PlistCreator('test', self._tmpdir) + plist_creator.build_tree_and_write_file() + assert os.path.exists(os.path.join(self._test_folder_path, 'Contents', 'info.plist'))