Skip to content

Commit 5372717

Browse files
authored
Merge pull request #592 from KhiopsML/try_fix_windows_without_env
fix: use sysconfig to locate khiops_env.cmd on Windows
2 parents 92478f1 + 62c02fe commit 5372717

3 files changed

Lines changed: 148 additions & 119 deletions

File tree

.github/workflows/pip.yml

Lines changed: 78 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ on:
1111
samples-revision:
1212
default: 11.0.0
1313
description: khiops-samples repo revision
14-
image-tag:
15-
default: latest
16-
description: Development Docker Image Tag
1714
pypi-target:
1815
type: choice
1916
default: None # no publishing to any Package Index by default
@@ -30,10 +27,7 @@ jobs:
3027
build:
3128
runs-on: ubuntu-22.04
3229
permissions:
33-
checks: write
3430
contents: read
35-
id-token: write
36-
packages: read
3731
steps:
3832
- name: Checkout sources
3933
uses: actions/checkout@v4
@@ -53,30 +47,63 @@ jobs:
5347
pip install build
5448
5549
# Build the source package (sdist) only
56-
python3 -m build --sdist
50+
python -m build --sdist
5751
- name: Upload the package as artifact
5852
uses: actions/upload-artifact@v4
5953
with:
6054
name: pip-package
6155
path: ./dist/khiops*.tar.gz
56+
# Test Pip package on brand new environments
6257
test:
6358
needs: build
64-
runs-on: ubuntu-22.04
6559
strategy:
6660
fail-fast: false
6761
matrix:
68-
container: [ubuntu22.04, rocky9, debian13]
69-
container:
70-
# 'latest' default image tag cannot be set as an environment variable,
71-
# because the `env` context is only accessible at the step level;
72-
# hence, it is hard-coded
73-
image: |-
74-
ghcr.io/khiopsml/khiops-python/khiopspydev-${{ matrix.container }}:${{ inputs.image-tag || 'latest' }}
62+
env:
63+
# Test without virtualenv only on platforms that support it out of the
64+
# box (i.e. where passing `--break-system-packages` to Pip is not
65+
# required)
66+
- {os: ubuntu-24.04, json-image: '{"image": null}', use-virtualenv: true}
67+
- {os: ubuntu-24.04, json-image: '{"image": null}', use-virtualenv: false}
68+
- {os: ubuntu-22.04, json-image: '{"image": "debian:trixie"}', use-virtualenv: true}
69+
- {os: ubuntu-22.04, json-image: '{"image": "rockylinux/rockylinux:10"}', use-virtualenv: true}
70+
- {os: ubuntu-22.04, json-image: '{"image": "rockylinux/rockylinux:10"}', use-virtualenv: false}
71+
- {os: windows-2025, json-image: '{"image": null}', use-virtualenv: true}
72+
- {os: windows-2025, json-image: '{"image": null}', use-virtualenv: false}
73+
- {os: macos-15, json-image: '{"image": null}', use-virtualenv: true}
74+
runs-on: ${{ matrix.env.os }}
75+
container: ${{ fromJSON(matrix.env.json-image) }}
7576
steps:
77+
- name: Make sure Python is installed on Linux (might not be in containers)
78+
if: fromJSON(matrix.env.json-image).image != null
79+
shell: sh
80+
run: |
81+
if command -v apt-get; then
82+
apt-get update -y
83+
apt-get install -y python3 python3-pip python3-venv
84+
elif command -v dnf; then
85+
dnf install -y python3 python3-pip
86+
# Install findutils on Rocky Linux when xargs cannot be found
87+
if ! command -v xargs; then
88+
dnf install -y findutils
89+
fi
90+
fi
7691
- name: Set parameters as env
92+
shell: bash
7793
run: |
7894
SAMPLES_REVISION=${{ inputs.samples-revision || env.DEFAULT_SAMPLES_REVISION }}
7995
echo "SAMPLES_REVISION=$SAMPLES_REVISION" >> "$GITHUB_ENV"
96+
if [[ $RUNNER_OS == 'Windows' ]]; then
97+
VENV_ACTIVATE_SCRIPT="khiops-venv/Scripts/activate"
98+
else
99+
VENV_ACTIVATE_SCRIPT="khiops-venv/bin/activate"
100+
fi
101+
echo "VENV_ACTIVATE_SCRIPT=$VENV_ACTIVATE_SCRIPT" >> "$GITHUB_ENV"
102+
# Setup Python (be portable wrt. python vs python3 in runners and
103+
# containers; either python3 or python is present as per the Python
104+
# installation step in the Docker container contexts)
105+
PYTHON=$(basename $(command -v python3 || command -v python))
106+
echo "PYTHON=$PYTHON" >> "$GITHUB_ENV"
80107
- name: Checkout sources # Checkout the sources to be able to extract the dependencies list
81108
uses: actions/checkout@v4
82109
with:
@@ -97,30 +124,37 @@ jobs:
97124
- name: Install the package
98125
shell: bash
99126
run: |
100-
# Allow Pip to write to its cache
101-
mkdir -p /github/home/.cache/pip
102-
chown -R $(whoami) /github/home/.cache/pip
127+
# Allow Pip to write to its cache (only applies to Docker containers)
128+
if [[ -n "${{ fromJSON(matrix.env.json-image).image }}" ]]; then
129+
mkdir -p /github/home/.cache/pip
130+
chown -R $(whoami) /github/home/.cache/pip
131+
fi
103132
104133
# Install the Khiops Python library
105134
106-
# A virtual env is mandatory under debian
107-
if [[ "${{ matrix.container }}" == "debian13" ]]; then
108-
python -m venv khiops-debian-venv
109-
source khiops-debian-venv/bin/activate
135+
# Use a virtualenv if required
136+
if [[ "${{ matrix.env.use-virtualenv }}" == "true" ]]; then
137+
$PYTHON -m venv khiops-venv
138+
source "$VENV_ACTIVATE_SCRIPT"
110139
fi
111-
pip install --upgrade pip
140+
$PYTHON -m pip install --upgrade pip
112141
# Add homogeneous TOML support (Python >= 3.12 has standard tomllib)
113-
pip install tomli
142+
$PYTHON -c "import tomllib" 2>/dev/null || $PYTHON -m pip install tomli
114143
# First, install all dependencies except khiops-core and khiops-drivers-*
115-
python scripts/extract_dependencies_from_pyproject_toml.py -f "pyproject.toml" --exclude-khiops-family > requires-no-khiops.txt
116-
pip install `cat requires-no-khiops.txt`
144+
$PYTHON scripts/extract_dependencies_from_pyproject_toml.py -f "pyproject.toml" --exclude-khiops-family > requires-no-khiops.txt
145+
[ ! -s requires-no-khiops.txt ] || xargs $PYTHON -m pip install < requires-no-khiops.txt
146+
# On Windows, the `impi-rt` MPI dependency must be installed
147+
# separately, from PyPI, because it is not present on TestPyPI
148+
if [[ $RUNNER_OS == 'Windows' ]]; then
149+
$PYTHON -m pip install impi-rt
150+
fi
117151
# khiops-core and khiops-drivers-* must always be installed from TestPyPI in order to avoid distorting usage statistics
118-
python scripts/extract_dependencies_from_pyproject_toml.py -f "pyproject.toml" --khiops-family-only > requires-khiops.txt
119-
pip install --index-url https://test.pypi.org/simple `cat requires-khiops.txt`
152+
$PYTHON scripts/extract_dependencies_from_pyproject_toml.py -f "pyproject.toml" --khiops-family-only > requires-khiops.txt
153+
[ ! -s requires-khiops.txt ] || xargs $PYTHON -m pip install --index-url https://test.pypi.org/simple < requires-khiops.txt
120154
rm -f requires-khiops.txt requires-no-khiops.txt
121155
# Lastly, install khiops-python
122-
pip install $(ls khiops*.tar.gz)
123-
if [[ "${{ matrix.container }}" == "debian13" ]]; then
156+
$PYTHON -m pip install khiops*.tar.gz
157+
if [[ "${{ matrix.env.use-virtualenv }}" == "true" ]]; then
124158
deactivate
125159
fi
126160
- name: Run tests
@@ -140,12 +174,13 @@ jobs:
140174
# modules are currently not initializing the shell anyway
141175
if [ -n "$MODULESHOME" ]; then module unload mpi; fi
142176
143-
# A virtual env is mandatory under debian
144-
if [[ "${{ matrix.container }}" == "debian13" ]]; then
145-
source khiops-debian-venv/bin/activate
177+
# Use a virtualenv if required
178+
if [[ "${{ matrix.env.use-virtualenv }}" == "true" ]]; then
179+
source "$VENV_ACTIVATE_SCRIPT"
146180
fi
147-
if [[ $RUNNER_OS != 'Windows' ]]; then
148-
# Set rights to OpenMPI-specific folder
181+
if [[ -n "${{ fromJSON(matrix.env.json-image).image }}" ]]; then
182+
# Set rights to OpenMPI-specific folder (only applies to Docker
183+
# containers)
149184
mkdir -p /github/home/.pmix/components
150185
chown -R $(whoami) /github/home/.pmix/components
151186
fi
@@ -163,7 +198,7 @@ jobs:
163198
# The MPI command is not always named mpiexec, but can be orterun etc
164199
# (as given by khiops_env)
165200
kh-status | grep "MPI command" | grep -vwq "<empty>"
166-
if [[ "${{ matrix.container }}" == "debian13" ]]; then
201+
if [[ "${{ matrix.env.use-virtualenv }}" == "true" ]]; then
167202
deactivate
168203
fi
169204
- name: Test package / Git tag version coherence
@@ -172,19 +207,18 @@ jobs:
172207
run: |
173208
# Don't exit on first error: print relevant error message
174209
set +e
175-
# A virtual env is mandatory under Debian 13
176-
if [[ "${{ matrix.container }}" == "debian13" ]]; then
177-
python -m venv khiops-debian-venv
178-
source khiops-debian-venv/bin/activate
210+
# Use a virtualenv if required
211+
if [[ "${{ matrix.env.use-virtualenv }}" == "true" ]]; then
212+
source "$VENV_ACTIVATE_SCRIPT"
179213
fi
180214
# Convert pre-release version specification in the Git tag to the Pip
181215
# format and check that it matches the Pip package version
182-
PACKAGE_VERSION=$(pip show khiops | awk 'BEGIN {FS = ": "} $1 ~ /^Version$/ {print $2}')
183-
if [[ "${{ matrix.container }}" == "debian13" ]]; then
216+
PACKAGE_VERSION=$($PYTHON -m pip show khiops | awk 'BEGIN {FS = ": "} $1 ~ /^Version$/ {print $2}')
217+
if [[ "${{ matrix.env.use-virtualenv }}" == "true" ]]; then
184218
deactivate
185219
fi
186-
echo ${{ github.ref_name }} | tr -d '-' | rev | sed -E 's/\.([^0-9].*)/\1/' | rev | \
187-
grep -wq $PACKAGE_VERSION
220+
echo "${{ github.ref_name }}" | tr -d '-' | rev | sed -E 's/\.([^0-9].*)/\1/' | rev | \
221+
grep -wq "$PACKAGE_VERSION"
188222
if [[ $? -ne 0 ]]
189223
then
190224
echo "::error::Python package version $PACKAGE_VERSION does not match Git tag ${{ github.ref_name }}"

.github/workflows/tests.yml

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
name: Tests
33
env:
44
DEFAULT_SAMPLES_REVISION: 11.0.0
5-
DEFAULT_KHIOPS_DESKTOP_REVISION: 11.0.0
65
on:
76
workflow_dispatch:
87
inputs:
@@ -12,9 +11,6 @@ on:
1211
image-tag:
1312
default: latest
1413
description: Development Docker Image Tag
15-
khiops-desktop-revision:
16-
default: 11.0.0
17-
description: Khiops Windows Desktop Application Version
1814
run-expensive-tests:
1915
type: boolean
2016
required: false
@@ -229,25 +225,6 @@ jobs:
229225
check-khiops-integration-on-windows:
230226
runs-on: windows-2022
231227
steps:
232-
- name: Download the Khiops Desktop NSIS Installer
233-
shell: pwsh
234-
run: |
235-
$KHIOPS_DESKTOP_REVISION = '${{ inputs.khiops-desktop-revision || env.DEFAULT_KHIOPS_DESKTOP_REVISION }}'
236-
$KHIOPS_DOWNLOAD_URL = "https://github.com/KhiopsML/khiops/releases/download/${KHIOPS_DESKTOP_REVISION}/khiops-${KHIOPS_DESKTOP_REVISION}-setup.exe"
237-
Invoke-WebRequest "${KHIOPS_DOWNLOAD_URL}" `
238-
-OutFile .\khiops-setup.exe `
239-
-UseBasicParsing
240-
Unblock-File .\khiops-setup.exe
241-
- name: Install the Khiops Desktop Application
242-
shell: pwsh
243-
run: |
244-
# Execute the installer
245-
$ErrorActionPreference = 'Stop'
246-
$ProgressPreference = 'SilentlyContinue'
247-
Start-Process `
248-
-FilePath .\khiops-setup.exe `
249-
-ArgumentList '/S' `
250-
-Wait
251228
- name: Checkout sources
252229
uses: actions/checkout@v4
253230
with:

khiops/core/internals/runner.py

Lines changed: 70 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import site
2121
import subprocess
2222
import sys
23+
import sysconfig
2324
import tempfile
2425
import uuid
2526
import warnings
@@ -955,53 +956,59 @@ def __init__(self):
955956

956957
def _initialize_khiops_environment(self):
957958
installation_method = _infer_khiops_installation_method()
958-
# in a 'pip' installation method,
959-
# the current Khiops Python library depends on the Khiops binary-only
960-
# PyPI package.
961-
# Let's ensure this dependency has not disappeared.
962-
if installation_method == "pip":
963-
try:
964-
distribution("khiops-core")
965-
except PackageNotFoundError as exc:
966-
raise KhiopsEnvironmentError(
967-
f"The Khiops binaries are not installed properly: {exc}. "
968-
"Re-install the Khiops Python library to automatically install "
969-
"Khiops. Go to https://khiops.org for more information.\n"
959+
match installation_method:
960+
# In conda-based environments, khiops_env is not in PATH;
961+
# its location must be inferred from the conda env directory.
962+
case "conda-based":
963+
khiops_env_path = os.path.join(
964+
_infer_env_bin_dir_for_conda_based_installations(), "khiops_env"
970965
)
971-
# Search for the `khiops_env` script location
972-
if platform.system() == "Windows":
973-
sys_executable_direct_parent = Path(sys.executable).parents[0]
974-
probable_khiops_env = os.path.join(
975-
sys_executable_direct_parent, "khiops_env.cmd"
976-
)
977-
# The script is found in the current environment
978-
if os.path.exists(probable_khiops_env):
979-
khiops_env_path = probable_khiops_env
980-
# Raise error otherwise
981-
else:
982-
raise KhiopsEnvironmentError(
983-
"No 'khiops_env.cmd' found in the current environment. "
984-
"Make sure you have installed Khiops properly. "
985-
"Go to https://khiops.org for more information."
986-
)
987-
# In Conda-based environments, `khiops_env` might not be in the PATH,
988-
# hence its path must be inferred
989-
elif installation_method == "conda-based":
990-
khiops_env_path = os.path.join(
991-
_infer_env_bin_dir_for_conda_based_installations(), "khiops_env"
992-
)
993-
if platform.system() == "Windows":
994-
khiops_env_path += ".cmd"
995-
996-
# On UNIX or Conda, khiops_env is always in path for a proper installation
997-
else:
998-
khiops_env_path = shutil.which("khiops_env")
999-
if khiops_env_path is None:
966+
if platform.system() == "Windows":
967+
khiops_env_path += ".cmd"
968+
# In an activated conda environment, khiops_env is in PATH.
969+
case "conda":
970+
khiops_env_path = self._infer_khiops_env_from_path(installation_method)
971+
case "pip":
972+
# Ensure the binary dependency is still installed.
973+
try:
974+
distribution("khiops-core")
975+
except PackageNotFoundError as exc:
976+
raise KhiopsEnvironmentError(
977+
f"The Khiops binaries are not installed properly: {exc}. "
978+
"Re-install the Khiops Python library to automatically install "
979+
"Khiops. Go to https://khiops.org for more information.\n"
980+
) from exc
981+
# On Windows, determine the Scripts directory where pip placed
982+
# khiops_env.cmd.
983+
# If this library is installed under the user site-packages
984+
# directory, khiops-core (and its khiops_env.cmd) was also installed
985+
# there with `pip install --user`, so use the user Scripts directory.
986+
# Otherwise use the standard Scripts directory (venv or system-wide
987+
# install). This avoids an ambiguous search and mirrors how pip
988+
# resolves scripts.
989+
if platform.system() == "Windows":
990+
library_root_dir_path = Path(__file__).parents[2]
991+
user_site_packages_path = Path(site.getusersitepackages())
992+
if library_root_dir_path.is_relative_to(user_site_packages_path):
993+
scripts_dir = sysconfig.get_path("scripts", "nt_user")
994+
else:
995+
scripts_dir = sysconfig.get_path("scripts")
996+
khiops_env_path = os.path.join(scripts_dir, "khiops_env.cmd")
997+
if not os.path.exists(khiops_env_path):
998+
raise KhiopsEnvironmentError(
999+
"No 'khiops_env.cmd' found in the current environment. "
1000+
"Make sure you have installed Khiops properly. "
1001+
"Go to https://khiops.org for more information."
1002+
)
1003+
# On UNIX, pip places khiops_env in the bin directory,
1004+
# which is in PATH.
1005+
else:
1006+
khiops_env_path = self._infer_khiops_env_from_path(
1007+
installation_method
1008+
)
1009+
case _:
10001010
raise KhiopsEnvironmentError(
1001-
"The 'khiops_env' script not found for the current "
1002-
f"'{installation_method}' installation method. Make sure "
1003-
"you have installed Khiops properly. "
1004-
"Go to https://khiops.org for more information."
1011+
f"Unknown installation method '{installation_method}'."
10051012
)
10061013

10071014
with subprocess.Popen(
@@ -1063,6 +1070,17 @@ def _initialize_khiops_environment(self):
10631070
# Initialize the default samples dir
10641071
self._initialize_default_samples_dir()
10651072

1073+
def _infer_khiops_env_from_path(self, installation_method):
1074+
khiops_env_path = shutil.which("khiops_env")
1075+
if khiops_env_path is None:
1076+
raise KhiopsEnvironmentError(
1077+
"The 'khiops_env' script not found for the current "
1078+
f"'{installation_method}' installation method. Make sure "
1079+
"you have installed Khiops properly. "
1080+
"Go to https://khiops.org for more information."
1081+
)
1082+
return khiops_env_path
1083+
10661084
def _initialize_default_samples_dir(self):
10671085
"""See class docstring"""
10681086
samples_dir = get_default_samples_dir()
@@ -1223,13 +1241,13 @@ def _detect_library_installation_incompatibilities(self, library_root_dir_path):
12231241
platform.system() == "Windows"
12241242
and
12251243
# Under Windows, there are two cases :
1226-
(
1227-
# for conda-based installations python is inside 'base_dir'
1228-
sys_executable_direct_parent != base_dir_path
1229-
and
1230-
# for 'pip' installations (within a virtual env)
1231-
# python is inside 'base_dir'/Scripts
1232-
sys_executable_grand_parent != base_dir_path
1244+
# for conda-based installations python is inside 'base_dir'
1245+
# for 'pip' installations (within a virtual env)
1246+
# python is inside 'base_dir'/Scripts
1247+
base_dir_path
1248+
not in (
1249+
sys_executable_direct_parent,
1250+
sys_executable_grand_parent,
12331251
)
12341252
# Under Linux or MacOS a bin/ folder exists
12351253
or sys_executable_grand_parent != base_dir_path

0 commit comments

Comments
 (0)