Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions doc/source/installer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,27 @@
#. **OS** supported for **Ubuntu(20.04 and 22.04)**.

#. Update ``apt-get`` repository and install the following packages with **sudo** privileges:
**wget, gnome, libffi-dev, libssl-dev, libsqlite3-dev, libxcb-xinerama0 and build-essential** packages with **sudo** privileges
**wget, gnome-terminal, libffi-dev, libssl-dev, libsqlite3-dev, libxcb-xinerama0 and build-essential** packages with **sudo** privileges

.. code:: shell

sudo apt-get update -y
sudo apt-get install wget gnome libffi-dev libssl-dev libsqlite3-dev libxcb-xinerama0 build-essential -y
sudo apt-get install wget gnome-terminal libffi-dev libssl-dev libsqlite3-dev libxcb-xinerama0 build-essential -y

.. note::

A terminal emulator is required for the ``Ansys Python Manager`` to run
commands (install packages, launch consoles, and so on). ``gnome-terminal``
is recommended, but ``konsole``, ``xfce4-terminal``, ``mate-terminal``,
``tilix``, and ``xterm`` are also supported. This is particularly relevant
when running under **WSL (Windows Subsystem for Linux)**, which does not
ship with a terminal emulator by default. In that case, install a
lightweight option instead of the full ``gnome-terminal`` package:

.. code:: shell

sudo apt-get update -y
sudo apt-get install xterm -y

#. Install **zlib** package

Expand Down Expand Up @@ -221,7 +236,7 @@

Now, instructions on how to install Python from the ``Ansys Python Manager`` are provided.

In order to do so, just follow the upcoming steps:

Check warning on line 239 in doc/source/installer.rst

View workflow job for this annotation

GitHub Actions / vale

[vale] doc/source/installer.rst#L239

[Google.WordListCase] Use 'to' instead of 'In order to'.
Raw output
{"message": "[Google.WordListCase] Use 'to' instead of 'In order to'.", "location": {"path": "doc/source/installer.rst", "range": {"start": {"line": 239, "column": 1}}}, "severity": "WARNING"}

#. Search for the ``Ansys Python Manager`` and run it.

Expand Down
6 changes: 3 additions & 3 deletions linux/debian/installer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ else
dependencies_available=false
fi
# check other dependencies
arr=("wget" "gnome" "libffi-dev" "libssl-dev" "build-essential" "libsqlite3-dev" "libxcb-xinerama0")
arr=("wget" "gnome-terminal" "libffi-dev" "libssl-dev" "build-essential" "libsqlite3-dev" "libxcb-xinerama0")
for x in "${arr[@]}"; do
c="dpkg -s $x >/dev/null 2>&1"
eval $c
Expand Down Expand Up @@ -75,7 +75,7 @@ else
dependencies_available=false
fi
# check other dependencies
arr=("wget" "gnome" "libffi-dev" "libssl-dev" "build-essential" "libsqlite3-dev" "libxcb-xinerama0")
arr=("wget" "gnome-terminal" "libffi-dev" "libssl-dev" "build-essential" "libsqlite3-dev" "libxcb-xinerama0")
for x in "${arr[@]}"; do
c="dpkg -s $x >/dev/null 2>&1"
eval $c
Expand Down Expand Up @@ -111,4 +111,4 @@ else
printf "Dependencies installation required sudo access.\n"
echo -e '\e]8;;https://installer.docs.pyansys.com/version/stable/installer.html\aFollow prerequisites in this link\e]8;;\a'
fi
fi
fi
5 changes: 4 additions & 1 deletion src/ansys/tools/installer/create_virtual_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,11 @@ def create_venv(self):
Path(venv_dir).mkdir(parents=True, exist_ok=True)
try:
self.cmd_create_venv(venv_dir)
except:
except Exception as err:
LOG.error(err)
self.failed_to_create_dialog()
self.update_table()
return

self.update_table()
self.venv_success_dialog()
Expand Down
47 changes: 41 additions & 6 deletions src/ansys/tools/installer/installed_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,12 +595,31 @@ def delete_virtual_environment(self, point):
subprocess.call(f'start /w /min cmd /K "{shell_cmd}"', shell=True)
if os.path.exists(parent_path):
shutil.rmtree(parent_path)
except:
pass
except Exception as err:
LOG.error(err)
if self._parent is not None and hasattr(self._parent, "show_error"):
self._parent.show_error(str(err))

# Finally, update the venv table
self.venv_table.update()

def _run_linux_or_report_error(self, func, *args, **kwargs):
"""Run a Linux command launcher, surfacing failures to the user.

Notes
-----
This is primarily needed to catch ``NoLinuxTerminalError``, which is
raised when no supported terminal emulator (such as ``gnome-terminal``)
is available. This is a common situation on WSL (Windows Subsystem for
Linux), where no terminal emulator is installed by default.
"""
try:
func(*args, **kwargs)
except Exception as err:
LOG.error(err)
if self._parent is not None and hasattr(self._parent, "show_error"):
self._parent.show_error(str(err))

def launch_cmd(
self,
extra: str = "",
Expand Down Expand Up @@ -672,7 +691,9 @@ def launch_cmd(
cmd = f"&& echo Python set to {py_path}"

if is_linux_os():
run_linux_command(py_path, extra, working_dir=working_dir)
self._run_linux_or_report_error(
run_linux_command, py_path, extra, working_dir=working_dir
)
else:
# Update the package managers
shell_cmd = f"set PATH={new_path} && python -m pip install --upgrade pip uv && exit"
Expand All @@ -687,7 +708,9 @@ def launch_cmd(
else:
cmd = f"&& echo Python set to {py_path}"
if is_linux_os():
run_linux_command(py_path, extra, True, working_dir=working_dir)
self._run_linux_or_report_error(
run_linux_command, py_path, extra, True, working_dir=working_dir
)
else:
shell_cmd = f'set PATH={myenv} && {py_path}\\Scripts\\activate.bat && cd /d ""{working_dir}"" {cmd}'
subprocess.call(f'start {min_win} cmd /K "{shell_cmd}"', shell=True)
Expand All @@ -702,7 +725,13 @@ def launch_cmd(
else:
cmd = f"&& echo Activating conda forge at path {py_path}"
if is_linux_os():
run_linux_command_conda(py_path, extra, True, working_dir=working_dir)
self._run_linux_or_report_error(
run_linux_command_conda,
py_path,
extra,
True,
working_dir=working_dir,
)
else:
shell_cmd = f'set PATH={myenv} && {miniforge_path}\\Scripts\\activate.bat && conda activate {py_path} && cd /d ""{working_dir}"" {cmd}'
subprocess.call(f'start {min_win} cmd /K "{shell_cmd}"', shell=True)
Expand All @@ -717,7 +746,13 @@ def launch_cmd(
else:
cmd = f"&& echo Activating conda forge at path {py_path}"
if is_linux_os():
run_linux_command_conda(py_path, extra, False, working_dir=working_dir)
self._run_linux_or_report_error(
run_linux_command_conda,
py_path,
extra,
False,
working_dir=working_dir,
)
else:
shell_cmd = f'set PATH={myenv} && {miniforge_path}\\Scripts\\activate.bat && conda activate {py_path} && cd /d ""{working_dir}"" {cmd}'
subprocess.call(f'start {min_win} cmd /K "{shell_cmd}"', shell=True)
120 changes: 103 additions & 17 deletions src/ansys/tools/installer/linux_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,60 @@
ansys_linux_path = f"/home/{user_name}/.local/ansys"


# Ordered list of supported terminal emulators, from most to least preferred.
# Each entry maps a terminal executable name to a callable that builds the
# ``argv`` list used to run ``command`` inside of it.
#
# Notes
# -----
# ``gnome-terminal`` is a client/server application: by default it forwards
# the request to a background ``gnome-terminal-server`` process and returns
# immediately, which is why the explicit ``--wait`` flag is required to block
# until the spawned command finishes. Most other terminal emulators (konsole,
# xfce4-terminal, xterm, etc.) keep running in the foreground by default, so
# blocking behavior comes "for free" when the process is not explicitly
# backgrounded.
_LINUX_TERMINALS = {
"gnome-terminal": lambda command, wait: (
["gnome-terminal"] + (["--wait"] if wait else []) + ["--", "sh", "-c", command]
),
"konsole": lambda command, wait: ["konsole", "-e", "sh", "-c", command],
"xfce4-terminal": lambda command, wait: (
["xfce4-terminal", "--disable-server", "-x", "sh", "-c", command]
),
"mate-terminal": lambda command, wait: (
["mate-terminal", "--disable-factory", "-x", "sh", "-c", command]
),
"tilix": lambda command, wait: ["tilix", "-e", "sh", "-c", command],
"xterm": lambda command, wait: ["xterm", "-e", "sh", "-c", command],
"x-terminal-emulator": lambda command, wait: (
["x-terminal-emulator", "-e", "sh", "-c", command]
),
}


class NoLinuxTerminalError(RuntimeError):
"""Raised when no supported terminal emulator is available on the system."""


def find_linux_terminal():
"""Find the first available, supported terminal emulator on this system.

Returns
-------
str or None
The name of the first supported terminal emulator found on the
``PATH``, or ``None`` if none of them are available. This is
commonly the case on WSL (Windows Subsystem for Linux) distributions,
which do not ship with a terminal emulator by default.

"""
for terminal in _LINUX_TERMINALS:
if shutil.which(terminal):
return terminal
return None


def is_linux_os():
"""
Create OS is Linux or Not.
Expand Down Expand Up @@ -160,17 +214,13 @@ def find_miniforge_linux(ansys_manager_installed_only=False):
paths = {}
if not ansys_manager_installed_only:
try:
subprocess.check_output("printenv | grep CONDA_PYTHON_EXE > /tmp/conda.txt")
with open("/tmp/conda.txt") as f:
conda_system_path = f.read()
conda_system_path = conda_system_path.replace("CONDA_PYTHON_EXE=", "")
conda_system_path = conda_system_path.replace("/bin/python", "").strip()
version = subprocess.check_output([f"conda", "--version"])
version = version.split()[1].decode("utf-8")
paths[conda_system_path] = (version, True)
os.remove("/tmp/conda.txt")
except:
pass
conda_system_path = os.environ["CONDA_PYTHON_EXE"]
conda_system_path = conda_system_path.replace("/bin/python", "").strip()
version = subprocess.check_output(["conda", "--version"])
version = version.split()[1].decode("utf-8")
paths[conda_system_path] = (version, True)
except Exception as e:
LOG.debug(e)
try:
version = subprocess.check_output(
[f"{ansys_linux_path}/conda/bin/conda", "--version"]
Expand Down Expand Up @@ -405,18 +455,54 @@ def query_gh_latest_release_linux(token=None):

def execute_linux_command(command, wait=True):
"""
Run linux command on gnome terminal.
Run a Linux command in the first available terminal emulator.

Previously this always shelled out to ``gnome-terminal``, which is not
installed by default on many Linux systems (for example, WSL
distributions), causing every action relying on this function to fail
silently. This now detects an available terminal emulator amongst
several common alternatives before running the command.

Parameters
----------
command : str
Command to run inside of the terminal.
wait : bool, default: True
Whether to block until the spawned terminal (and command) finishes.

Raises
------
NoLinuxTerminalError
If no supported terminal emulator could be found on the ``PATH``.

Examples
--------
>>> execute_linux_command("ls")

"""
wait_command = ""
if wait:
wait_command = "--wait"
LOG.debug(f"gnome-terminal {wait_command} -- sh -c '{command}'")
os.system(f"gnome-terminal {wait_command} -- sh -c '{command}'")
terminal = find_linux_terminal()
if terminal is None:
msg = (
"No supported terminal emulator was found on this system (tried: "
f"{', '.join(_LINUX_TERMINALS)}). Ansys Python Manager requires one "
"of these to run commands. This is a common issue on WSL (Windows "
"Subsystem for Linux), which does not install a terminal emulator "
"by default. Install one, for example with: sudo apt-get install xterm"
)
LOG.error(msg)
raise NoLinuxTerminalError(msg)

argv = _LINUX_TERMINALS[terminal](command, wait)
LOG.debug("Executing linux command with %s: %s", terminal, argv)
try:
if wait:
subprocess.run(argv)
else:
subprocess.Popen(argv, start_new_session=True)
except Exception as err:
msg = f"Failed to execute command using {terminal}: {err}"
LOG.error(msg)
raise NoLinuxTerminalError(msg) from err


def get_os_version():
Expand Down
18 changes: 11 additions & 7 deletions src/ansys/tools/installer/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,17 @@ def _uninstall(self):
if self.uninstall_window_cache_remove_configs_checkbox.isChecked():
self._remove_configs()

os_version = get_os_version()
if os_version in ["centos", "fedora"]:
script_path = os.path.join(ASSETS_PATH, "uninstaller_yum.sh")
execute_linux_command(f"{script_path}", wait=False)
elif get_os_version().startswith("2"):
script_path = os.path.join(ASSETS_PATH, "uninstaller_ubuntu.sh")
execute_linux_command(f"{script_path}", wait=False)
try:
os_version = get_os_version()
if os_version in ["centos", "fedora"]:
script_path = os.path.join(ASSETS_PATH, "uninstaller_yum.sh")
execute_linux_command(f"{script_path}", wait=False)
elif get_os_version().startswith("2"):
script_path = os.path.join(ASSETS_PATH, "uninstaller_ubuntu.sh")
execute_linux_command(f"{script_path}", wait=False)
except Exception as e:
self._parent.show_error(str(e))
return

self.user_confirmation_form.close()
self._parent.uninstall_window.close()
Expand Down
48 changes: 48 additions & 0 deletions tests/test_linux_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import pytest

from ansys.tools.installer.linux_functions import (
NoLinuxTerminalError,
execute_linux_command,
find_linux_terminal,
get_conda_url_and_filename,
get_vanilla_url_and_filename,
run_linux_command,
Expand Down Expand Up @@ -52,3 +57,46 @@ def test_run_linux_command_accepts_working_dir():

sig_conda = inspect.signature(run_linux_command_conda)
assert "working_dir" in sig_conda.parameters


def test_find_linux_terminal_returns_none_when_no_terminal_available(monkeypatch):
"""No terminal emulator should be found when none are on the PATH."""
monkeypatch.setattr(
"ansys.tools.installer.linux_functions.shutil.which", lambda _name: None
)
assert find_linux_terminal() is None


def test_find_linux_terminal_finds_non_gnome_terminal(monkeypatch):
"""A non gnome-terminal emulator (e.g. xterm) should still be detected."""
monkeypatch.setattr(
"ansys.tools.installer.linux_functions.shutil.which",
lambda name: "/usr/bin/xterm" if name == "xterm" else None,
)
assert find_linux_terminal() == "xterm"


def test_execute_linux_command_raises_clear_error_without_terminal(monkeypatch):
"""execute_linux_command should raise a clear, actionable error (e.g. on WSL)."""
monkeypatch.setattr(
"ansys.tools.installer.linux_functions.shutil.which", lambda _name: None
)
with pytest.raises(NoLinuxTerminalError):
execute_linux_command("echo hello")


def test_execute_linux_command_uses_available_terminal(monkeypatch):
"""execute_linux_command should use whichever supported terminal is found."""
calls = []
monkeypatch.setattr(
"ansys.tools.installer.linux_functions.shutil.which",
lambda name: "/usr/bin/xterm" if name == "xterm" else None,
)
monkeypatch.setattr(
"ansys.tools.installer.linux_functions.subprocess.run",
lambda argv: calls.append(argv),
)
execute_linux_command("echo hello", wait=True)
assert len(calls) == 1
assert calls[0][0] == "xterm"
assert "echo hello" in calls[0]
Loading