Skip to content

Commit 5dd3b93

Browse files
authored
Merge pull request #518 from MEDomicsLab/fix_supersetVenvMacOS
Fix superset venv mac os
2 parents 6ab3047 + c85bf1e commit 5dd3b93

9 files changed

Lines changed: 168 additions & 39 deletions

File tree

go_server/main

-2.95 KB
Binary file not shown.

pythonCode/modules/superset/SupersetEnvManager.py

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
11
import json
22
import subprocess
33
import sys
4+
import os
45
import time
56
from pathlib import Path
7+
import venv
68

79
SUPERSET_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

1417
class 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:

pythonCode/modules/superset/create_user.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,25 @@ def _custom_process(self, json_config: dict) -> dict:
3434

3535
# Map settings
3636
python_path = json_config["pythonPath"]
37+
app_data_path = json_config.get("appDataPath")
38+
39+
if not app_data_path:
40+
# Fallback to default locations if not provided (for backward compatibility or testing)
41+
if sys.platform == "win32":
42+
app_data_path = str(Path(os.getenv('APPDATA')) / "medomics-platform")
43+
elif sys.platform == "darwin":
44+
app_data_path = str(Path.home() / "Library/Application Support/medomics-platform")
45+
else:
46+
app_data_path = str(Path.home() / ".config/medomics-platform")
47+
3748
username = json_config["username"]
3849
firstname = json_config["firstname"]
3950
lastname = json_config["lastname"]
4051
email = json_config["email"]
4152
password = json_config["password"]
4253

4354
# Set up Superset
44-
output = self.create_user(python_path, username, firstname, lastname, email, password)
55+
output = self.create_user(python_path, app_data_path, username, firstname, lastname, email, password)
4556

4657
return output
4758

@@ -59,12 +70,13 @@ def run_command(self, command, env=None, capture_output=True, timeout=None):
5970

6071
return {}
6172

62-
def create_user(self, python_path: str, username: str, firstname: str, lastname: str, email: str, password: str):
73+
def create_user(self, python_path: str, app_data_path: str, username: str, firstname: str, lastname: str, email: str, password: str):
6374
"""
6475
Creates a new Superset user.
6576
6677
Args:
6778
python_path: The path to the Python installation.
79+
app_data_path: The path to the application data directory.
6880
username: The username of the new user.
6981
firstname: The first name of the new user.
7082
lastname: The last name of the new user.
@@ -76,7 +88,7 @@ def create_user(self, python_path: str, username: str, firstname: str, lastname:
7688
"""
7789
# Prepare environment variables
7890
env = os.environ.copy()
79-
manager = SupersetEnvManager(python_path)
91+
manager = SupersetEnvManager(python_path, app_data_path)
8092
path_superset = manager.get_superset_path()
8193
env["FLASK_APP"] = "superset"
8294

@@ -90,7 +102,7 @@ def create_user(self, python_path: str, username: str, firstname: str, lastname:
90102
"password": password,
91103
}
92104
output = self.run_command(
93-
f"{path_superset} fab create-admin "
105+
f'"{path_superset}" fab create-admin '
94106
f"--username {admin_user['username']} "
95107
f"--firstname {admin_user['firstname']} "
96108
f"--lastname {admin_user['lastname']} "

pythonCode/modules/superset/launch.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,24 @@ def __init__(self, json_params: dict, _id: str = None):
2828

2929
def _custom_process(self, json_config: dict) -> dict:
3030
"""
31-
This function predicts from a model, a dataset, and a new dataset
31+
This function is called to execute the custom process which sets up Superset.
3232
"""
3333
# Map settings
3434
port = json_config["port"]
3535
python_path = json_config["pythonPath"]
36+
app_data_path = json_config.get("appDataPath")
37+
38+
if not app_data_path:
39+
# Fallback to default locations if not provided (for backward compatibility or testing)
40+
if sys.platform == "win32":
41+
app_data_path = str(Path(os.getenv('APPDATA')) / "medomics-platform")
42+
elif sys.platform == "darwin":
43+
app_data_path = str(Path.home() / "Library/Application Support/medomics-platform")
44+
else:
45+
app_data_path = str(Path.home() / ".config/medomics-platform")
3646

3747
# Set up Superset
38-
result = self.setup_superset(port, python_path)
48+
result = self.setup_superset(port, python_path, app_data_path)
3949

4050
return result
4151

@@ -53,13 +63,14 @@ def run_command(self, command, env=None, capture_output=True, timeout=None):
5363
return {"error": f"Error while running command: {command}. Full error log:" + e.stderr}
5464

5565

56-
def setup_superset(self, port, python_path):
66+
def setup_superset(self, port, python_path, app_data_path):
5767
"""
5868
Set up Superset with the provided settings.
5969
6070
Args:
6171
scripts_path (path): The path to the Python scripts directory.
6272
port (path): The port on which to run Superset.
73+
app_data_path (path): The path to the application data directory.
6374
6475
Returns:
6576
A dictionary containing the error message, if any.
@@ -70,7 +81,7 @@ def setup_superset(self, port, python_path):
7081

7182
# Check if the virtual environment exists
7283
self.set_progress(now=progress, label="Checking the Superset virtual environment...")
73-
manager = SupersetEnvManager(python_path)
84+
manager = SupersetEnvManager(python_path, app_data_path)
7485
if not manager.check_env_exists():
7586
print("Creating Superset virtual environment...")
7687
self.set_progress(now=self._progress["now"]+step, label="Creating Superset virtual environment...")
@@ -96,7 +107,13 @@ def setup_superset(self, port, python_path):
96107
# Generate a private key
97108
print("Generating a private key...")
98109
self.set_progress(label="Checking the private key...")
99-
private_key = subprocess.check_output("openssl rand -base64 42", shell=True, text=True).strip()
110+
try:
111+
import secrets
112+
private_key = secrets.token_urlsafe(42)
113+
except ImportError:
114+
import base64
115+
private_key = base64.b64encode(os.urandom(42)).decode('utf-8')
116+
100117
if private_key is None:
101118
print("Error while generating a private key.")
102119
return {"error": "Error while generating a private key."}
@@ -151,7 +168,7 @@ def setup_superset(self, port, python_path):
151168
# Initialize the database
152169
print("Initializing the Superset database...")
153170
self.set_progress(now=self._progress["now"]+step, label="Initializing the Superset database...")
154-
result = self.run_command(f"{superset_path} db upgrade", env)
171+
result = self.run_command(f'"{superset_path}" db upgrade', env)
155172
if "error" in result:
156173
return result
157174

@@ -166,7 +183,7 @@ def setup_superset(self, port, python_path):
166183
"password": "admin",
167184
}
168185
result = self.run_command(
169-
f"{superset_path} fab create-admin "
186+
f'"{superset_path}" fab create-admin '
170187
f"--username {admin_user['username']} "
171188
f"--firstname {admin_user['firstname']} "
172189
f"--lastname {admin_user['lastname']} "
@@ -180,10 +197,18 @@ def setup_superset(self, port, python_path):
180197
# Initialize Superset
181198
print("Initializing Superset...")
182199
self.set_progress(now=self._progress["now"]+2*step, label="Initializing Superset...")
183-
result = self.run_command(f"{superset_path} init", env)
200+
result = self.run_command(f'"{superset_path}" init', env)
184201
if "error" in result:
185202
return result
186203

204+
# Load examples (optional)
205+
print("Loading example data...")
206+
self.set_progress(now=self._progress["now"]+step, label="Loading default example data (this may take several minutes)...")
207+
result = self.run_command(f'"{superset_path}" load_examples', env)
208+
if "error" in result:
209+
print(f"Warning: Failed to load examples: {result.get('error')}")
210+
# We continue even if examples fail to load, as it is optional
211+
187212
# Check if port is available
188213
print(f"Checking if port {port} is available...")
189214
self.set_progress(now=self._progress["now"]+step, label="Checking if port is available...")
@@ -202,7 +227,7 @@ def setup_superset(self, port, python_path):
202227
print(f"Launching Superset on port {port}...")
203228
self.set_progress(now=self._progress["now"]+step, label="Launching Superset...")
204229
try:
205-
subprocess.Popen(f"{superset_path} run -p {port}", shell=True, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
230+
subprocess.Popen(f'"{superset_path}" run -p {port}', shell=True, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
206231
except subprocess.CalledProcessError as e:
207232
print(f"Error while running command: {f'{superset_path} run -p {port}'}")
208233
print(e.stderr)

renderer/components/input/MEDprofiles/MEDprofilesPrepareData.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-unreachable */
12
import { randomUUID } from "crypto"
23
import { DataFrame } from "../../../utilities/danfo.js"
34
import { Button } from 'primereact/button'
@@ -54,7 +55,7 @@ const MEDprofilesPrepareData = () => {
5455
className="mx-3"
5556
/>
5657
</div>
57-
)
58+
)
5859
const [binaryFileList, setBinaryFileList] = useState([]) // list of available binary files
5960
const [binaryFilename, setBinaryFilename] = useState("MEDprofiles_bin.pkl") // name under which the MEDprofiles binary file will be saved
6061
const [generatedClassesFolder, setGeneratedClassesFolder] = useState(null) // folder containing the generated MEDclasses

0 commit comments

Comments
 (0)