From 40bedc6e018f7f85a97b40cf28cdb5a6c9afde8e Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Tue, 21 Nov 2023 23:31:46 +0800 Subject: [PATCH 01/12] Update format.py Automatically download GTK3 libs for cairocffi if necessary --- mathicsscript/format.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/mathicsscript/format.py b/mathicsscript/format.py index 3b2e769..edafece 100644 --- a/mathicsscript/format.py +++ b/mathicsscript/format.py @@ -5,7 +5,9 @@ from typing import Callable import math import networkx as nx +import os import random +import requests from tempfile import NamedTemporaryFile from mathics.core.atoms import String @@ -27,6 +29,37 @@ PyMathicsGraph = Symbol("Pymathics`Graph") +cairo_libs_path = "./libs" + +def download_file(url, dest): + response = requests.get(url, timeout=5) + if response.status_code == 200: + with open(dest, 'wb') as file: + file.write(response.content) + print(f"Downloaded: {dest}") + else: + print(f"Failed to download: {url}") + +def download_cairo_libs(): + branch_url = "https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/tree/master/" + directory_url = "gtk-nsis-pack/bin" + response = requests.get(branch_url + directory_url + "?recursive=1", timeout = 5) + if response.status_code == 200: + data = response.json() + for item in data['payload']['tree']['items']: + if '.dll' not in item['name']: + continue + file_url = branch_url + item['path'] + file_path = os.path.join(cairo_libs_path, item['name']) + download_file(file_url, file_path) + else: + print("Failed to retrieve directory contents.") + + +if sys.version_info >= (3, 8) and os.name == 'nt' and not os.path.exists(cairo_libs_path): + os.makedirs(cairo_libs_path) + download_cairo_libs() + try: from matplotlib import __version__ as matplotlib_version except ImportError: From ae671e1cf681e7e743d7a9317a370da46a6e77d4 Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Tue, 21 Nov 2023 23:32:37 +0800 Subject: [PATCH 02/12] Update format.py --- mathicsscript/format.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mathicsscript/format.py b/mathicsscript/format.py index edafece..8e12884 100644 --- a/mathicsscript/format.py +++ b/mathicsscript/format.py @@ -8,6 +8,7 @@ import os import random import requests +import sys from tempfile import NamedTemporaryFile from mathics.core.atoms import String From ab2f448ebefd6c3db8d3f303bf244e7c15423f83 Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Tue, 21 Nov 2023 23:35:55 +0800 Subject: [PATCH 03/12] Update format.py --- mathicsscript/format.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mathicsscript/format.py b/mathicsscript/format.py index 8e12884..3e13a8b 100644 --- a/mathicsscript/format.py +++ b/mathicsscript/format.py @@ -56,10 +56,23 @@ def download_cairo_libs(): else: print("Failed to retrieve directory contents.") +def set_dll_search_path(): + # Python 3.8+ no longer searches for DLLs in PATH, so we have to add + # everything in PATH manually. Note that unlike PATH add_dll_directory + # has no defined order, so if there are two cairo DLLs in PATH we + # might get a random one. + if os.name != "nt" or not hasattr(os, "add_dll_directory"): + return + try: + os.add_dll_directory(cairo_libs_path) + except OSError: + pass + if sys.version_info >= (3, 8) and os.name == 'nt' and not os.path.exists(cairo_libs_path): os.makedirs(cairo_libs_path) download_cairo_libs() + set_dll_search_path() try: from matplotlib import __version__ as matplotlib_version From 01f9b2597badeefbac2925d069c8d13d29824fe7 Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:25:56 +0800 Subject: [PATCH 04/12] Create fixcairo.py --- mathicsscript/fixcairo.py | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 mathicsscript/fixcairo.py diff --git a/mathicsscript/fixcairo.py b/mathicsscript/fixcairo.py new file mode 100644 index 0000000..c7140ab --- /dev/null +++ b/mathicsscript/fixcairo.py @@ -0,0 +1,51 @@ +""" +Fix cairo for Windows, Python 3.8+ +""" + +import os +import requests +import sys + +cairo_libs_path = "./libs" + +def download_file(url, dest): + response = requests.get(url, timeout=5) + if response.status_code == 200: + with open(dest, 'wb') as file: + file.write(response.content) + print(f"Downloaded: {dest}") + else: + print(f"Failed to download: {url}") + +def download_cairo_libs(): + branch_url = "https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/tree/master/" + directory_url = "gtk-nsis-pack/bin" + response = requests.get(branch_url + directory_url + "?recursive=1", timeout = 5) + if response.status_code == 200: + data = response.json() + for item in data['payload']['tree']['items']: + if '.dll' not in item['name']: + continue + file_url = branch_url + item['path'] + file_path = os.path.join(cairo_libs_path, item['name']) + download_file(file_url, file_path) + else: + print("Failed to retrieve directory contents.") + +def set_dll_search_path(): + # Python 3.8+ no longer searches for DLLs in PATH, so we have to add + # everything in PATH manually. Note that unlike PATH add_dll_directory + # has no defined order, so if there are two cairo DLLs in PATH we + # might get a random one. + if os.name != "nt" or not hasattr(os, "add_dll_directory"): + return + try: + os.add_dll_directory(cairo_libs_path) + except OSError: + pass + + +if sys.version_info >= (3, 8) and os.name == 'nt' and not os.path.exists(cairo_libs_path): + os.makedirs(cairo_libs_path) + download_cairo_libs() + set_dll_search_path() From bb0fa86ccfeec4ea8d7f60a5a2f832174392598b Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:26:03 +0800 Subject: [PATCH 05/12] Update format.py --- mathicsscript/format.py | 47 ----------------------------------------- 1 file changed, 47 deletions(-) diff --git a/mathicsscript/format.py b/mathicsscript/format.py index 3e13a8b..3b2e769 100644 --- a/mathicsscript/format.py +++ b/mathicsscript/format.py @@ -5,10 +5,7 @@ from typing import Callable import math import networkx as nx -import os import random -import requests -import sys from tempfile import NamedTemporaryFile from mathics.core.atoms import String @@ -30,50 +27,6 @@ PyMathicsGraph = Symbol("Pymathics`Graph") -cairo_libs_path = "./libs" - -def download_file(url, dest): - response = requests.get(url, timeout=5) - if response.status_code == 200: - with open(dest, 'wb') as file: - file.write(response.content) - print(f"Downloaded: {dest}") - else: - print(f"Failed to download: {url}") - -def download_cairo_libs(): - branch_url = "https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/tree/master/" - directory_url = "gtk-nsis-pack/bin" - response = requests.get(branch_url + directory_url + "?recursive=1", timeout = 5) - if response.status_code == 200: - data = response.json() - for item in data['payload']['tree']['items']: - if '.dll' not in item['name']: - continue - file_url = branch_url + item['path'] - file_path = os.path.join(cairo_libs_path, item['name']) - download_file(file_url, file_path) - else: - print("Failed to retrieve directory contents.") - -def set_dll_search_path(): - # Python 3.8+ no longer searches for DLLs in PATH, so we have to add - # everything in PATH manually. Note that unlike PATH add_dll_directory - # has no defined order, so if there are two cairo DLLs in PATH we - # might get a random one. - if os.name != "nt" or not hasattr(os, "add_dll_directory"): - return - try: - os.add_dll_directory(cairo_libs_path) - except OSError: - pass - - -if sys.version_info >= (3, 8) and os.name == 'nt' and not os.path.exists(cairo_libs_path): - os.makedirs(cairo_libs_path) - download_cairo_libs() - set_dll_search_path() - try: from matplotlib import __version__ as matplotlib_version except ImportError: From 364c654ded9afd8b6fd3f6253b75513250acd004 Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Wed, 22 Nov 2023 23:03:24 +0800 Subject: [PATCH 06/12] Update setup.py Add dependence of tqdm --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a802b73..0ae3928 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ def read(*rnames): # "mathics_pygments @ https://github.com/Mathics3/mathics-pygments/archive/master.zip#egg=mathics_pygments", "mathics_pygments>=1.0.2", "term-background >= 1.0.1", + 'tqdm', ], entry_points={ "console_scripts": [ From f75c8e65ee87f76e16b2feaf842bb7b1b5824de7 Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:26:48 +0800 Subject: [PATCH 07/12] Update fixcairo.py --- mathicsscript/fixcairo.py | 134 +++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 31 deletions(-) diff --git a/mathicsscript/fixcairo.py b/mathicsscript/fixcairo.py index c7140ab..3aea44c 100644 --- a/mathicsscript/fixcairo.py +++ b/mathicsscript/fixcairo.py @@ -1,51 +1,123 @@ +# -*- coding: utf-8 -*- """ -Fix cairo for Windows, Python 3.8+ +Fix cairo for Windows """ import os +import platform +import subprocess +from ctypes.util import find_library +from importlib.util import find_spec + import requests -import sys +from tqdm import tqdm + +default_GTK_path = "C:\\Program Files\\GTK3-Runtime Win64\\bin" + +mathicsscript_path = find_spec("mathicsscript").submodule_search_locations[0] + +if platform.architecture()[0] == "64bit": + release_url = "https://api.github.com/repos/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases/latest" +else: + release_url = "https://api.github.com/repos/miegir/GTK-for-Windows-Runtime-Environment-Installer-32/releases/latest" + +fix_cairo_long_intro = f""" +We noticed that you are using mathicsscript on Windows. Due to a well known bug in the dependency +package cairocffi for mathicsscript on Windows, mathicsscript is not working properly right now. +We will now try to fix it, or you can visit https://github.com/Mathics3/mathicsscript/issues/76 +to fix it yourself. + +Specifically, cairocffi needs libcairo-2.dll and some other dll files to work properly on Windows. +To get these libraries, we will download the latest GTK+ for Windows Runtime Environment Installer +from {release_url} +and install it for you. After installation, whenever you run mathicsscript, the GTK+ for Windows +dll libraries will be automatically loaded to make mathicsscript run properly. + +IMPORTANT: Remember to check the "Set up PATH environment variable to include GTK+" option! -cairo_libs_path = "./libs" +Please note that now the GTK+ for Windows dll libraries will only be loaded when mathicsscript is +imported. If you want to load them when importing cairocffi, see +https://github.com/Mathics3/mathicsscript/issues/76. -def download_file(url, dest): - response = requests.get(url, timeout=5) +Do you want us to install GTK+ for Windows Runtime Environment for you? [Y/n] (default: Y) +""" + + +def download_file(url, dest, name=None): + response = requests.get(url, stream=True, timeout=5) + total_size = int(response.headers.get("content-length", 0)) if response.status_code == 200: - with open(dest, 'wb') as file: - file.write(response.content) - print(f"Downloaded: {dest}") + with open(dest, "wb") as file: + with tqdm( + total=total_size, + unit="B", + unit_scale=True, + unit_divisor=1024, + desc=f"Downloading {name}", + ) as pbar: + for chunk in response.iter_content(chunk_size=1024): + if chunk: + file.write(chunk) + file.flush() + pbar.update(len(chunk)) + pbar.refresh() else: print(f"Failed to download: {url}") -def download_cairo_libs(): - branch_url = "https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/tree/master/" - directory_url = "gtk-nsis-pack/bin" - response = requests.get(branch_url + directory_url + "?recursive=1", timeout = 5) + +def download_GTK_installer(): + response = requests.get(release_url, timeout=5) if response.status_code == 200: - data = response.json() - for item in data['payload']['tree']['items']: - if '.dll' not in item['name']: - continue - file_url = branch_url + item['path'] - file_path = os.path.join(cairo_libs_path, item['name']) - download_file(file_url, file_path) + name = response.json()["assets"][0]["name"] + file_url = response.json()["assets"][0]["browser_download_url"] + file_path = os.path.join(mathicsscript_path, name) + download_file(file_url, file_path, name) + print("Done.") + return file_path else: print("Failed to retrieve directory contents.") + def set_dll_search_path(): - # Python 3.8+ no longer searches for DLLs in PATH, so we have to add - # everything in PATH manually. Note that unlike PATH add_dll_directory - # has no defined order, so if there are two cairo DLLs in PATH we - # might get a random one. - if os.name != "nt" or not hasattr(os, "add_dll_directory"): + """ + Python 3.8+ no longer searches for DLLs in PATH, so we have to add + everything in PATH manually. Note that unlike PATH add_dll_directory + has no defined order, so if there are two cairo DLLs in PATH we + might get a random one. + """ + if not hasattr(os, "add_dll_directory"): return - try: - os.add_dll_directory(cairo_libs_path) - except OSError: - pass + for path in os.environ.get("PATH", "").split(os.pathsep): + try: + os.add_dll_directory(path) + except OSError: + pass -if sys.version_info >= (3, 8) and os.name == 'nt' and not os.path.exists(cairo_libs_path): - os.makedirs(cairo_libs_path) - download_cairo_libs() +def fix_cairo(): set_dll_search_path() + if find_library("libcairo-2"): + return + else: + choice = input(fix_cairo_long_intro).lower() + if choice == "y" or choice == "": + GTK_installer = os.path.join( + mathicsscript_path, "gtk3-runtime-3.24.31-2022-01-04-ts-win64.exe" + ) + # download_GTK_installer() + GTK_install_cmd = os.path.join(mathicsscript_path, "install_GTK.cmd") + with open(os.path.join(mathicsscript_path, "install_GTK.cmd"), "w") as file: + file.write(f'"{GTK_installer}"') + subprocess.run( + GTK_install_cmd + ) # Allow users to run installer as administrators + os.remove(GTK_install_cmd) + set_dll_search_path() + if os.path.exists(default_GTK_path): + os.environ["PATH"] += os.pathsep + default_GTK_path + if find_library("libcairo-2"): + print("Successfully fixed cairocffi for mathicsscript.") + else: + print( + "Please restart the current terminal to make the changes take effect." + ) From c424acd8f59934efc4520ccf254b0982b0daa32f Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:27:10 +0800 Subject: [PATCH 08/12] Update __init__.py --- mathicsscript/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mathicsscript/__init__.py b/mathicsscript/__init__.py index d12f2c9..ec26856 100644 --- a/mathicsscript/__init__.py +++ b/mathicsscript/__init__.py @@ -4,11 +4,15 @@ Copyright 2020-2021 The Mathics Team """ +from ctypes.util import find_library + +from mathicsscript.fixcairo import fix_cairo from mathicsscript.version import __version__ def load_default_settings_files(definitions): import os.path as osp + from mathics.core.definitions import autoload_files root_dir = osp.realpath(osp.dirname(__file__)) @@ -17,3 +21,6 @@ def load_default_settings_files(definitions): __all__ = ["__version__", "load_default_settings_files"] + +if not find_library("libcairo-2"): + fix_cairo() From 45f3f9c4865c155e4fc719713b3b54dffd016713 Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:36:49 +0800 Subject: [PATCH 09/12] Update fixcairo.py --- mathicsscript/fixcairo.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/mathicsscript/fixcairo.py b/mathicsscript/fixcairo.py index 3aea44c..a85215d 100644 --- a/mathicsscript/fixcairo.py +++ b/mathicsscript/fixcairo.py @@ -73,7 +73,7 @@ def download_GTK_installer(): file_path = os.path.join(mathicsscript_path, name) download_file(file_url, file_path, name) print("Done.") - return file_path + return file_path # path to GTK installer else: print("Failed to retrieve directory contents.") @@ -101,16 +101,13 @@ def fix_cairo(): else: choice = input(fix_cairo_long_intro).lower() if choice == "y" or choice == "": - GTK_installer = os.path.join( - mathicsscript_path, "gtk3-runtime-3.24.31-2022-01-04-ts-win64.exe" - ) - # download_GTK_installer() + GTK_installer = download_GTK_installer() GTK_install_cmd = os.path.join(mathicsscript_path, "install_GTK.cmd") with open(os.path.join(mathicsscript_path, "install_GTK.cmd"), "w") as file: file.write(f'"{GTK_installer}"') subprocess.run( GTK_install_cmd - ) # Allow users to run installer as administrators + ) # Allow users to run installer as administrators. Tried to run it directly with subprocess.run(['runas', '/user:Administrator', ...]) and failed :( os.remove(GTK_install_cmd) set_dll_search_path() if os.path.exists(default_GTK_path): From f58f1b8babed53f088ea6b12f4fbdf40432fdc0e Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:28:17 +0800 Subject: [PATCH 10/12] Update fixcairo.py --- mathicsscript/fixcairo.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/mathicsscript/fixcairo.py b/mathicsscript/fixcairo.py index a85215d..a3da694 100644 --- a/mathicsscript/fixcairo.py +++ b/mathicsscript/fixcairo.py @@ -12,8 +12,6 @@ import requests from tqdm import tqdm -default_GTK_path = "C:\\Program Files\\GTK3-Runtime Win64\\bin" - mathicsscript_path = find_spec("mathicsscript").submodule_search_locations[0] if platform.architecture()[0] == "64bit": @@ -73,7 +71,7 @@ def download_GTK_installer(): file_path = os.path.join(mathicsscript_path, name) download_file(file_url, file_path, name) print("Done.") - return file_path # path to GTK installer + return file_path else: print("Failed to retrieve directory contents.") @@ -94,6 +92,32 @@ def set_dll_search_path(): pass +def search_folders(root_path, folder_name): + found_folders = [] + with os.scandir(root_path) as entries: + for entry in entries: + if entry.is_dir() and folder_name in entry.name: + found_folders.append(entry.path) + return found_folders + + +def search_file_in_folders(folders, file_name): + found_paths = [] + for folder_path in folders: + file_paths = search_file_recursive(folder_path, file_name) + found_paths.extend(file_paths) + return found_paths + + +def search_file_recursive(folder_path, file_name): + found_paths = [] + for dirpath, _, filenames in os.walk(folder_path): + for filename in filenames: + if filename == file_name: + found_paths.append(dirpath) + return found_paths + + def fix_cairo(): set_dll_search_path() if find_library("libcairo-2"): @@ -110,8 +134,11 @@ def fix_cairo(): ) # Allow users to run installer as administrators. Tried to run it directly with subprocess.run(['runas', '/user:Administrator', ...]) and failed :( os.remove(GTK_install_cmd) set_dll_search_path() - if os.path.exists(default_GTK_path): - os.environ["PATH"] += os.pathsep + default_GTK_path + # Manually add GTK+ for Windows dll libraries to os.environ["PATH"] + GTK_folders = search_folders("C:\\Program Files", "GTK") + GTK_dll_paths = search_file_in_folders(GTK_folders, "libcairo-2.dll") + if GTK_dll_paths: + os.environ["PATH"] += os.pathsep + GTK_dll_paths[0] if find_library("libcairo-2"): print("Successfully fixed cairocffi for mathicsscript.") else: From a6621f37c26cac405bad68768ca288a1cae2ba15 Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Sat, 25 Nov 2023 18:35:06 +0800 Subject: [PATCH 11/12] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0ae3928..5e36bb1 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ def read(*rnames): # "mathics_pygments @ https://github.com/Mathics3/mathics-pygments/archive/master.zip#egg=mathics_pygments", "mathics_pygments>=1.0.2", "term-background >= 1.0.1", - 'tqdm', + "tqdm; platform_system=='Windows'", ], entry_points={ "console_scripts": [ From dc1d44b7f456c6e0bb3fb49aa04b4042583b58ff Mon Sep 17 00:00:00 2001 From: Li Xiang <54926635+Li-Xiang-Ideal@users.noreply.github.com> Date: Mon, 27 Nov 2023 01:16:56 +0800 Subject: [PATCH 12/12] Update fixcairo.py Forgot to remove tqdm's dependency on system other than Windows before :( --- mathicsscript/fixcairo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mathicsscript/fixcairo.py b/mathicsscript/fixcairo.py index a3da694..8cece51 100644 --- a/mathicsscript/fixcairo.py +++ b/mathicsscript/fixcairo.py @@ -10,7 +10,8 @@ from importlib.util import find_spec import requests -from tqdm import tqdm +if os.name == "nt": + from tqdm import tqdm mathicsscript_path = find_spec("mathicsscript").submodule_search_locations[0]