Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix cairo for Windows #77

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
7 changes: 7 additions & 0 deletions mathicsscript/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__))
Expand All @@ -17,3 +21,6 @@ def load_default_settings_files(definitions):


__all__ = ["__version__", "load_default_settings_files"]

if not find_library("libcairo-2"):
fix_cairo()
Copy link
Member

@rocky rocky Nov 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of thing is better done in setup.py rather than as part of the that gets run on every invocation of mathicsscript.

Copy link
Author

@Li-Xiang-Ideal Li-Xiang-Ideal Nov 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll move the GTK+ installation part to setup.py when I have time. As for set_dll_search_path(), it should get run on every invocation of mathicsscript unless cairocffi itself is fixed. I've put in a PR to cairocffi and if they approve it, everything can be done in setup.py.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've put in a PR to cairocffi and if they approve it, everything can be done in setup.py.

Awesome!

147 changes: 147 additions & 0 deletions mathicsscript/fixcairo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# -*- coding: utf-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
from tqdm import tqdm

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!

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.

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:
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_GTK_installer():
response = requests.get(release_url, timeout=5)
if response.status_code == 200:
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 not hasattr(os, "add_dll_directory"):
return
for path in os.environ.get("PATH", "").split(os.pathsep):
try:
os.add_dll_directory(path)
except OSError:
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"):
return
else:
choice = input(fix_cairo_long_intro).lower()
if choice == "y" or choice == "":
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. Tried to run it directly with subprocess.run(['runas', '/user:Administrator', ...]) and failed :(
os.remove(GTK_install_cmd)
set_dll_search_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:
print(
"Please restart the current terminal to make the changes take effect."
)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a dependency that is only needed for Windows right now.

],
entry_points={
"console_scripts": [
Expand Down