Skip to content

Commit f04f76b

Browse files
committed
Changes venv creation path for superset to writeable location. Fix for MacOS venv creation permission denied.
1 parent 08c61d7 commit f04f76b

File tree

5 files changed

+97
-29
lines changed

5 files changed

+97
-29
lines changed

go_server/main

1.34 KB
Binary file not shown.

pythonCode/modules/superset/SupersetEnvManager.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
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",
@@ -12,14 +14,18 @@
1214
]
1315

1416
class SupersetEnvManager:
15-
def __init__(self, python_path):
17+
def __init__(self, python_path, app_data_path):
1618
self.python_path = python_path
1719
self.env_path = None
20+
21+
# Using app_data_path to define environment path (defined by userData https://www.electronjs.org/docs/latest/api/app#appgetapppath)
22+
# This is a writeable path where we can create our virtual environment (Fix for macOS venv creation issues)
23+
base_dir = Path(app_data_path)
1824
if sys.platform == "win32":
19-
self.env_path = Path(python_path).parent / "superset_env/Scripts/python.exe"
25+
self.env_path = base_dir / "superset_env/Scripts/python.exe"
2026
else:
21-
self.env_path = python_path.replace("bin", "bin/superset_env/bin")
22-
27+
self.env_path = base_dir / "superset_env/bin/python"
28+
2329
def check_env_exists(self):
2430
"""Check if the virtual environment exists"""
2531
if sys.platform == "win32":
@@ -34,17 +40,38 @@ def create_env(self):
3440
env_name = str(self.env_path)[:env_name+len("superset_env")]
3541
else:
3642
return False
43+
44+
print(f"Creating virtual environment at: {env_name}")
45+
46+
try:
47+
# Create venv without pip first to avoid crash due to missing lib on macOS standalone builds
48+
venv.create(env_name, with_pip=False, clear=True)
49+
50+
# Fix for macOS standalone python: symlink libpython
51+
if sys.platform == "darwin":
52+
base_python_lib = Path(self.python_path).parent.parent / "lib"
53+
venv_lib = Path(env_name) / "lib"
54+
55+
# Find libpython dylib
56+
lib_files = list(base_python_lib.glob("libpython*.dylib"))
57+
if lib_files:
58+
target_lib = lib_files[0]
59+
link_name = venv_lib / target_lib.name
60+
if not link_name.exists():
61+
try:
62+
link_name.symlink_to(target_lib)
63+
print(f"Symlinked {target_lib} to {link_name}")
64+
except Exception as e:
65+
print(f"Failed to symlink libpython: {e}")
66+
67+
# Now install pip
68+
print("Installing pip...")
69+
subprocess.run([str(self.env_path), "-m", "ensurepip"], check=True)
3770

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:
4471
print(f"Environment created at: {self.env_path}")
4572
return True
46-
else:
47-
print(f"Error creating environment: {process.stderr}")
73+
except Exception as e:
74+
print(f"Error creating environment: {e}")
4875
return False
4976

5077
def check_requirements(self):

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: 23 additions & 11 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...")
@@ -147,7 +158,7 @@ def setup_superset(self, port, python_path):
147158
# Initialize the database
148159
print("Initializing the Superset database...")
149160
self.set_progress(now=self._progress["now"]+step, label="Initializing the Superset database...")
150-
result = self.run_command(f"{superset_path} db upgrade", env)
161+
result = self.run_command(f'"{superset_path}" db upgrade', env)
151162
if "error" in result:
152163
return result
153164

@@ -162,7 +173,7 @@ def setup_superset(self, port, python_path):
162173
"password": "admin",
163174
}
164175
result = self.run_command(
165-
f"{superset_path} fab create-admin "
176+
f'"{superset_path}" fab create-admin '
166177
f"--username {admin_user['username']} "
167178
f"--firstname {admin_user['firstname']} "
168179
f"--lastname {admin_user['lastname']} "
@@ -176,16 +187,17 @@ def setup_superset(self, port, python_path):
176187
# Initialize Superset
177188
print("Initializing Superset...")
178189
self.set_progress(now=self._progress["now"]+step, label="Initializing Superset...")
179-
result = self.run_command(f"{superset_path} init", env)
190+
result = self.run_command(f'"{superset_path}" init', env)
180191
if "error" in result:
181192
return result
182193

183194
# Load examples (optional)
184195
print("Loading example data...")
185-
self.set_progress(now=self._progress["now"]+step, label="Loading default example data...")
186-
result = self.run_command(f"{superset_path} load_examples", env)
196+
self.set_progress(now=self._progress["now"]+step, label="Loading default example data (this may take several minutes)...")
197+
result = self.run_command(f'"{superset_path}" load_examples', env)
187198
if "error" in result:
188-
return result
199+
print(f"Warning: Failed to load examples: {result.get('error')}")
200+
# We continue even if examples fail to load, as it is optional
189201

190202
# Check if port is available
191203
print(f"Checking if port {port} is available...")
@@ -205,7 +217,7 @@ def setup_superset(self, port, python_path):
205217
print(f"Launching Superset on port {port}...")
206218
self.set_progress(now=self._progress["now"]+step, label="Launching Superset...")
207219
try:
208-
subprocess.Popen(f"{superset_path} run -p {port}", shell=True, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
220+
subprocess.Popen(f'"{superset_path}" run -p {port}', shell=True, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
209221
except subprocess.CalledProcessError as e:
210222
print(f"Error while running command: {f'{superset_path} run -p {port}'}")
211223
print(e.stderr)

renderer/components/mainPages/superset/SupersetFrame.jsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,15 @@ const SupersetDashboard = () => {
8484
async function launchSuperset() {
8585
let freePort = 8080 // in the future maybe we'll use getPort() from get-port package
8686
let pythonPath = await ipcRenderer.invoke("getBundledPythonEnvironment")
87+
let appDataPath = await ipcRenderer.invoke("appGetPath", "userData")
8788

8889
// Send the request to the backend
8990
let jsonToSend = {
9091
"port": freePort,
9192
"pythonPath": pythonPath,
93+
"appDataPath": appDataPath,
9294
}
95+
console.log("Launching superset with python path:", pythonPath)
9396
setLoading(true)
9497
requestBackend(
9598
port,
@@ -136,10 +139,12 @@ const SupersetDashboard = () => {
136139
async function createUser() {
137140
// get Python path
138141
let pythonPath = await ipcRenderer.invoke("getBundledPythonEnvironment")
142+
let appDataPath = await ipcRenderer.invoke("appGetPath", "userData")
139143

140144
// Send the request to the backend
141145
let jsonToSend = {
142146
"pythonPath": pythonPath,
147+
"appDataPath": appDataPath,
143148
"username": newUserUsername,
144149
"password": newUserPassword,
145150
"firstname": newFirstName,
@@ -309,9 +314,20 @@ const SupersetDashboard = () => {
309314
return
310315
}
311316

312-
const getEnvPath = (pythonPath) => {
317+
const getEnvPath = (pythonPath, appDataPath) => {
313318
if (!pythonPath) return ''
314319

320+
// If appDataPath is provided, use it to construct the path
321+
if (appDataPath) {
322+
const isWindows = os.platform() === "win32"
323+
if (isWindows) {
324+
return `${appDataPath}\\superset_env\\Scripts\\python.exe`
325+
} else {
326+
return `${appDataPath}/superset_env/bin/python`
327+
}
328+
}
329+
330+
// Fallback to old logic if appDataPath is not available (should not happen with new logic)
315331
const isWindows = os.platform() === "win32"
316332
if (isWindows) {
317333
const pathParts = pythonPath.split(/[\\/]/)
@@ -323,7 +339,8 @@ const SupersetDashboard = () => {
323339
}
324340

325341
const getSupersetConfigPath = async (pythonPath) => {
326-
const envPath = getEnvPath(pythonPath)
342+
let appDataPath = await ipcRenderer.invoke("appGetPath", "userData")
343+
const envPath = getEnvPath(pythonPath, appDataPath)
327344
if (!envPath) return ''
328345

329346
const isWindows = os.platform() === "win32"

0 commit comments

Comments
 (0)