11import json
22import subprocess
33import sys
4+ import os
45import time
56from pathlib import Path
7+ import venv
68
79SUPERSET_PACKAGES = [
810 "apache-superset==4.1.1" ,
911 "flask-cors==5.0.0" ,
1012 "marshmallow==3.26.1" ,
11- "psycopg2-binary==2.9.9"
13+ "psycopg2-binary==2.9.9" ,
14+ "wtforms==2.3.3" , # https://github.com/apache/superset/issues/29289#issuecomment-2341321222
1215]
1316
1417class SupersetEnvManager :
15- def __init__ (self , python_path ):
18+ def __init__ (self , python_path , app_data_path ):
1619 self .python_path = python_path
1720 self .env_path = None
21+
22+ # Using app_data_path to define environment path (defined by userData https://www.electronjs.org/docs/latest/api/app#appgetapppath)
23+ # This is a writeable path where we can create our virtual environment (Fix for macOS venv creation issues)
24+ base_dir = Path (app_data_path )
1825 if sys .platform == "win32" :
19- self .env_path = Path ( python_path ). parent / "superset_env/Scripts/python.exe"
20- else :
26+ self .env_path = base_dir / "superset_env/Scripts/python.exe"
27+ elif sys . platform == "linux" :
2128 self .env_path = python_path .replace ("bin" , "bin/superset_env/bin" )
22-
29+ else :
30+ self .env_path = base_dir / "superset_env/bin/python"
31+
2332 def check_env_exists (self ):
2433 """Check if the virtual environment exists"""
2534 if sys .platform == "win32" :
@@ -34,17 +43,48 @@ def create_env(self):
3443 env_name = str (self .env_path )[:env_name + len ("superset_env" )]
3544 else :
3645 return False
46+
47+ print (f"Creating virtual environment at: { env_name } " )
48+
49+ try :
50+ # Fix for macOS standalone python: symlink libpython
51+ if sys .platform == "darwin" :
52+ # Create venv without pip first to avoid crash due to missing lib on macOS standalone builds
53+ venv .create (env_name , with_pip = False , clear = True )
54+
55+ base_python_lib = Path (self .python_path ).parent .parent / "lib"
56+ venv_lib = Path (env_name ) / "lib"
57+
58+ # Find libpython dylib
59+ lib_files = list (base_python_lib .glob ("libpython*.dylib" ))
60+ if lib_files :
61+ target_lib = lib_files [0 ]
62+ link_name = venv_lib / target_lib .name
63+ if not link_name .exists ():
64+ try :
65+ link_name .symlink_to (target_lib )
66+ print (f"Symlinked { target_lib } to { link_name } " )
67+ except Exception as e :
68+ print (f"Failed to symlink libpython: { e } " )
69+
70+ # Now install pip
71+ print ("Installing pip..." )
72+ subprocess .run ([str (self .env_path ), "-m" , "ensurepip" ], check = True )
3773
38- # Create virtual environment
39- process = subprocess .Popen ([
40- self .python_path , "-m" , "venv" , env_name
41- ], stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True )
42- process .wait ()
43- if process .returncode == 0 :
74+ else :
75+ # Create virtual environment
76+ process = subprocess .Popen ([
77+ self .python_path , "-m" , "venv" , env_name
78+ ], stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True )
79+ process .wait ()
80+ if process .returncode != 0 :
81+ print (f"Error creating environment: { process .stderr } " )
82+ raise Exception (f"Failed to create virtual environment. Error: { process .stderr } " )
83+
4484 print (f"Environment created at: { self .env_path } " )
4585 return True
46- else :
47- print (f"Error creating environment: { process . stderr } " )
86+ except Exception as e :
87+ print (f"Error creating environment: { e } " )
4888 return False
4989
5090 def check_requirements (self ):
@@ -82,11 +122,11 @@ def get_installed_packages(self):
82122 str (self .python_path ), "-m" , "pip" , "freeze"
83123 ], capture_output = True , text = True , check = False )
84124
85- packages = {}
125+ packages = []
86126 for line in result .stdout .split ('\n ' ):
87127 if '==' in line :
88128 name , version = line .split ('==' , 1 )
89- packages [ name . lower ()] = version . strip ( )
129+ packages . append ({ 'name' : name , ' version' : version } )
90130 return packages
91131
92132 def is_package_installed (self , package_name , installed_packages = None ):
@@ -102,8 +142,20 @@ def is_package_installed(self, package_name, installed_packages=None):
102142
103143 def install_requirements (self ):
104144 """Install packages in the environment"""
145+ # Upgrade pip, setuptools and wheel first to ensure we can install binary wheels
146+ try :
147+ subprocess .run (
148+ [str (self .env_path ), "-m" , "pip" , "install" , "--upgrade" , "--prefer-binary" , "pip" , "setuptools" , "wheel" ],
149+ check = True ,
150+ capture_output = True ,
151+ text = True
152+ )
153+ except subprocess .CalledProcessError as e :
154+ print (f"Warning: Failed to upgrade pip/setuptools/wheel: { e .stderr } " )
155+
105156 # Build install command
106- install_cmd = [str (self .env_path ), "-m" , "pip" , "install" ]
157+ # Use --prefer-binary to avoid compiling from source if possible (fixes issues on machines without build tools)
158+ install_cmd = [str (self .env_path ), "-m" , "pip" , "install" , "--prefer-binary" ]
107159
108160 for requirement in SUPERSET_PACKAGES :
109161 if isinstance (requirement , dict ):
@@ -153,11 +205,23 @@ def install_packages(self, set_progress: callable, current_progress: int, step:
153205 print (f"Failed to ensure pip: { result .stderr } " )
154206 return False
155207
208+ # Upgrade pip, setuptools and wheel first
209+ try :
210+ subprocess .run (
211+ [str (self .env_path ), "-m" , "pip" , "install" , "--upgrade" , "--prefer-binary" , "pip" , "setuptools" , "wheel" ],
212+ check = True ,
213+ capture_output = True ,
214+ text = True
215+ )
216+ except subprocess .CalledProcessError as e :
217+ print (f"Warning: Failed to upgrade pip/setuptools/wheel: { e .stderr } " )
218+
156219 success = True
157220 for package in SUPERSET_PACKAGES :
158221 set_progress (label = f"Installing { package .split ('==' )[0 ]} ..." )
222+ # Use --prefer-binary to avoid compiling from source
159223 result = subprocess .run ([
160- str (self .env_path ), "-m" , "pip" , "install" , package
224+ str (self .env_path ), "-m" , "pip" , "install" , "--prefer-binary" , package
161225 ], check = True , capture_output = True , text = True )
162226
163227 if result .returncode == 0 :
0 commit comments