Skip to content

Commit 170417a

Browse files
authored
Remove custom python commands from EXE installers (#1089)
* Remove python from base environment where possible * Fix NSIS template * Add news file * Only exit with error if base needs python * Remove Python logic from NSIS if Python is not in the base environment * Explain why return code 2 is used in _base_needs_python * Always set register_python_default * Consolidate different path removal options * Remove duplicate code * Unify uninstallation workflow * Always show /AddToPath example in help text * Add post-solve check for mamba * Do not check for mamba 1 because mamba 1 depends on Python * Add clarifying comments * Remove unused installer_type parameter * Restore base environment python check * Fix syntax error
1 parent c2aa220 commit 170417a

File tree

7 files changed

+134
-58
lines changed

7 files changed

+134
-58
lines changed

constructor/fcp.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,9 @@ def _solve_precs(
234234
conda_exe="conda.exe",
235235
extra_env=False,
236236
input_dir="",
237+
base_needs_python=True,
237238
):
238-
# Add python to specs, since all installers need a python interpreter. In the future we'll
239-
# probably want to add conda too.
240-
# JRG: This only applies to the `base` environment; `extra_envs` are exempt
241-
if not extra_env:
239+
if not extra_env and base_needs_python:
242240
specs = (*specs, "python")
243241
if environment:
244242
logger.debug("specs: <from existing environment '%s'>", environment)
@@ -312,8 +310,8 @@ def _solve_precs(
312310
if python_prec:
313311
precs.remove(python_prec)
314312
precs.insert(0, python_prec)
315-
elif not extra_env:
316-
# the base environment must always have python; this has been addressed
313+
elif not extra_env and base_needs_python:
314+
# the base environment may require python; this has been addressed
317315
# at the beginning of _main() but we can still get here through the
318316
# environment_file option
319317
sys.exit("python MUST be part of the base environment")
@@ -392,6 +390,7 @@ def _main(
392390
extra_envs=None,
393391
check_path_spaces=True,
394392
input_dir="",
393+
base_needs_python=True,
395394
):
396395
precs = _solve_precs(
397396
name,
@@ -408,6 +407,7 @@ def _main(
408407
verbose=verbose,
409408
conda_exe=conda_exe,
410409
input_dir=input_dir,
410+
base_needs_python=base_needs_python,
411411
)
412412
extra_envs = extra_envs or {}
413413
conda_in_base: PackageCacheRecord = next((prec for prec in precs if prec.name == "conda"), None)
@@ -496,6 +496,7 @@ def main(info, verbose=True, dry_run=False, conda_exe="conda.exe"):
496496
transmute_file_type = info.get("transmute_file_type", "")
497497
extra_envs = info.get("extra_envs", {})
498498
check_path_spaces = info.get("check_path_spaces", True)
499+
base_needs_python = info.get("_win_install_needs_python_exe", False)
499500

500501
if not channel_urls and not channels_remap and not (environment or environment_file):
501502
sys.exit("Error: at least one entry in 'channels' or 'channels_remap' is required")
@@ -548,6 +549,7 @@ def main(info, verbose=True, dry_run=False, conda_exe="conda.exe"):
548549
extra_envs,
549550
check_path_spaces,
550551
input_dir,
552+
base_needs_python,
551553
)
552554

553555
info["_all_pkg_records"] = pkg_records # full PackageRecord objects

constructor/main.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import json
1616
import logging
1717
import os
18+
import subprocess
1819
import sys
1920
from os.path import abspath, expanduser, isdir, join
2021
from pathlib import Path
@@ -76,6 +77,18 @@ def get_output_filename(info):
7677
)
7778

7879

80+
def _win_install_needs_python_exe(conda_exe: str) -> bool:
81+
results = subprocess.run(
82+
[conda_exe, "constructor", "windows", "--help"],
83+
capture_output=True,
84+
check=False,
85+
)
86+
# Argparse uses return code 2 if a subcommand does not exist
87+
# If the windows subcommand does not exist, python.exe is still
88+
# required in the base environment.
89+
return results.returncode == 2
90+
91+
7992
def main_build(
8093
dir_path,
8194
output_dir=".",
@@ -275,6 +288,9 @@ def is_conda_meta_frozen(path_str: str) -> bool:
275288
"enable_currentUserHome": "true",
276289
}
277290

291+
if osname == "win":
292+
info["_win_install_needs_python_exe"] = _win_install_needs_python_exe(info["_conda_exe"])
293+
278294
info["installer_type"] = itypes[0]
279295
fcp_main(info, verbose=verbose, dry_run=dry_run, conda_exe=conda_exe)
280296
if dry_run:

constructor/nsis/main.nsi.tmpl

Lines changed: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,12 @@ ${Using:StrFunc} StrStr
7979
!define ARCH {{ arch }}
8080
!define PLATFORM {{ installer_platform }}
8181
!define CONSTRUCTOR_VERSION {{ constructor_version }}
82+
{%- if has_python %}
8283
!define PY_VER {{ pyver_components[:2] | join(".") }}
8384
!define PYVERSION_JUSTDIGITS {{ pyver_components | join("") }}
8485
!define PYVERSION {{ pyver_components | join(".") }}
8586
!define PYVERSION_MAJOR {{ pyver_components[0] }}
87+
{%- endif %}
8688
!define DEFAULT_PREFIX {{ default_prefix }}
8789
!define DEFAULT_PREFIX_DOMAIN_USER {{ default_prefix_domain_user }}
8890
!define DEFAULT_PREFIX_ALL_USERS {{ default_prefix_all_users }}
@@ -304,7 +306,9 @@ FunctionEnd
304306
/InstallationType=AllUsers [default: JustMe]$\n\
305307
/AddToPath=[0|1] [default: 0]$\n\
306308
/KeepPkgCache=[0|1] [default: {{ 1 if keep_pkgs else 0 }}]$\n\
309+
{%- if has_python %}
307310
/RegisterPython=[0|1] [default: AllUsers: 1, JustMe: 0]$\n\
311+
{%- endif %}
308312
/NoRegistry=[0|1] [default: AllUsers: 0, JustMe: 0]$\n\
309313
/NoScripts=[0|1] [default: 0]$\n\
310314
/NoShortcuts=[0|1] [default: 0]$\n\
@@ -323,9 +327,14 @@ FunctionEnd
323327
Install for all users, but don't add to PATH env var:$\n\
324328
> $EXEFILE /InstallationType=AllUsers$\n\
325329
$\n\
330+
{%- if has_python %}
326331
Install for just me, add to PATH and register as system Python:$\n\
327332
> $EXEFILE /RegisterPython=1 /AddToPath=1$\n\
328333
$\n\
334+
{%- endif %}
335+
Install for just me and add to PATH:$\n\
336+
> $EXEFILE /AddToPath=1$\n\
337+
$\n\
329338
Install for just me, with no registry modification (for CI):$\n\
330339
> $EXEFILE /NoRegistry=1$\n\
331340
$\n\
@@ -349,6 +358,7 @@ FunctionEnd
349358
${EndIf}
350359

351360
ClearErrors
361+
{%- if has_python %}
352362
${GetOptions} $ARGV "/RegisterPython=" $ARGV_RegisterPython
353363
${IfNot} ${Errors}
354364
${If} $ARGV_RegisterPython = "1"
@@ -357,6 +367,7 @@ FunctionEnd
357367
StrCpy $Ana_RegisterSystemPython_State ${BST_UNCHECKED}
358368
${EndIf}
359369
${EndIf}
370+
{%- endif %}
360371

361372
ClearErrors
362373
${GetOptions} $ARGV "/KeepPkgCache=" $ARGV_KeepPkgCache
@@ -1142,6 +1153,7 @@ Function OnDirectoryLeave
11421153
UnicodePathTest::UnicodePathTest $INSTDIR
11431154
Pop $R1
11441155

1156+
{%- if has_python %}
11451157
# Python 3 can be installed in a CP_ACP path until MKL is Unicode capable.
11461158
# (mkl_rt.dll calls LoadLibraryA() to load mkl_intel_thread.dll)
11471159
# Python 2 can only be installed to an ASCII path.
@@ -1159,6 +1171,7 @@ Function OnDirectoryLeave
11591171
abort
11601172

11611173
valid_path:
1174+
{%- endif %}
11621175

11631176
Push $R1
11641177
${IsWritable} $INSTDIR $R1
@@ -1241,6 +1254,50 @@ FunctionEnd
12411254
!insertmacro AbortRetryNSExecWaitMacro ""
12421255
!insertmacro AbortRetryNSExecWaitMacro "un."
12431256

1257+
{%- set pathname = "$INSTDIR\\condabin" if initialize_conda == "condabin" else "$INSTDIR\\Scripts & Library\\bin" %}
1258+
!macro AddRemovePath add_remove un
1259+
{# python.exe is required if conda-standalone does not support the windows subcommand (<25.11.x) #}
1260+
{%- if needs_python_exe %}
1261+
${If} ${add_remove} == "add"
1262+
{%- if initialize_conda == 'condabin' %}
1263+
${Print} "Adding {{ pathname }} PATH..."
1264+
StrCpy $R0 "addcondabinpath"
1265+
{%- else %}
1266+
${Print} "Adding {{ pathname }} to PATH..."
1267+
StrCpy $R0 "addpath ${PYVERSION} ${NAME} ${VERSION} ${ARCH}"
1268+
{%- endif %}
1269+
StrCpy $R1 "Failed to add {{ NAME }} to PATH"
1270+
${Else}
1271+
${Print} "Running rmpath script..."
1272+
StrCpy $R0 "rmpath"
1273+
StrCpy $R1 "Failed to remove {{ NAME }} from PATH"
1274+
${EndIf}
1275+
${If} ${Silent}
1276+
push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" $R0'
1277+
${Else}
1278+
push '"$INSTDIR\python.exe" -E -s "$INSTDIR\Lib\_nsis.py" $R0'
1279+
${EndIf}
1280+
push $R1
1281+
push 'WithLog'
1282+
call ${un}AbortRetryNSExecWait
1283+
{%- else %}
1284+
{%- set pathflag = "--condabin" if initialize_conda == "condabin" else "--classic" %}
1285+
${If} ${add_remove} == "add"
1286+
${Print} "Adding {{ pathname }} to PATH..."
1287+
StrCpy $R0 "prepend"
1288+
StrCpy $R1 'Failed to add {{ NAME }} to PATH'
1289+
${Else}
1290+
${Print} "Removing {{ pathname }} from PATH..."
1291+
StrCpy $R0 "remove"
1292+
StrCpy $R1 'Failed to remove {{ NAME }} from PATH'
1293+
${EndIf}
1294+
push '"$INSTDIR\_conda.exe" constructor windows path --$R0=user --prefix "$INSTDIR" {{ pathflag }}'
1295+
push $R1
1296+
push 'WithLog'
1297+
call ${un}AbortRetryNSExecWait
1298+
{%- endif %}
1299+
!macroend
1300+
12441301
!macro setInstdirPermissions
12451302
# To address CVE-2022-26526.
12461303
# Revoke the write permission on directory "$INSTDIR" for Users. Users are:
@@ -1338,9 +1395,11 @@ Section "Install"
13381395
# for users even during an all-users installation.
13391396
!insertmacro setInstdirPermissions
13401397

1398+
{% if needs_python_exe %}
13411399
SetOutPath "$INSTDIR\Lib"
13421400
File "{{ NSIS_DIR }}\_nsis.py"
13431401
File "{{ NSIS_DIR }}\_system_path.py"
1402+
{% endif %}
13441403

13451404
{%- if has_license %}
13461405
SetOutPath "$INSTDIR"
@@ -1533,20 +1592,14 @@ Section "Install"
15331592
${EndIf}
15341593

15351594
{% if initialize_conda %}
1536-
${If} $Ana_AddToPath_State = ${BST_CHECKED}
1537-
{%- if initialize_conda == 'condabin' %}
1538-
${Print} "Adding $INSTDIR\condabin to PATH..."
1539-
push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" addcondabinpath'
1540-
{%- else %}
1541-
${Print} "Adding $INSTDIR\Scripts & Library\bin to PATH..."
1542-
push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" addpath ${PYVERSION} ${NAME} ${VERSION} ${ARCH}'
1543-
{%- endif %}
1544-
push 'Failed to add {{ NAME }} to PATH'
1545-
push 'WithLog'
1546-
call AbortRetryNSExecWait
1595+
${If} ${FileExists} "$INSTDIR\.nonadmin"
1596+
${If} $Ana_AddToPath_State = ${BST_CHECKED}
1597+
!insertmacro AddRemovePath "add" ""
1598+
${EndIf}
15471599
${EndIf}
15481600
{%- endif %}
15491601

1602+
{%- if has_python %}
15501603
# Create registry entries saying this is the system Python
15511604
# (for this version)
15521605
!define PYREG "Software\Python\PythonCore\${PY_VER}"
@@ -1564,6 +1617,7 @@ Section "Install"
15641617
WriteRegStr SHCTX "${PYREG}\PythonPath" \
15651618
"" "$INSTDIR\Lib;$INSTDIR\DLLs"
15661619
${EndIf}
1620+
{%- endif %}
15671621

15681622
${If} $ARGV_NoRegistry == "0"
15691623
# Registry uninstall info
@@ -1594,21 +1648,6 @@ Section "Install"
15941648
${Print} "Done!"
15951649
SectionEnd
15961650

1597-
!macro AbortRetryNSExecWaitLibNsisCmd cmd
1598-
SetDetailsPrint both
1599-
${Print} "Running ${cmd} scripts..."
1600-
SetDetailsPrint listonly
1601-
${If} ${Silent}
1602-
push '"$INSTDIR\pythonw.exe" -E -s "$INSTDIR\Lib\_nsis.py" ${cmd}'
1603-
${Else}
1604-
push '"$INSTDIR\python.exe" -E -s "$INSTDIR\Lib\_nsis.py" ${cmd}'
1605-
${EndIf}
1606-
push "Failed to run ${cmd}"
1607-
push 'WithLog'
1608-
call un.AbortRetryNSExecWait
1609-
SetDetailsPrint both
1610-
!macroend
1611-
16121651
Section "Uninstall"
16131652
${LogSet} on
16141653
${If} ${Silent}
@@ -1660,7 +1699,7 @@ Section "Uninstall"
16601699
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_NAME", "${NAME}").r0'
16611700
StrCpy $0 ${VERSION}
16621701
${If} $INSTALLER_VERSION != ""
1663-
StrCpy $0 $INSTALLER_VERSION
1702+
StrCpy $0 $INSTALLER_VERSION
16641703
${EndIf}
16651704
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_VER", "$0").r0'
16661705
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_PLAT", "${PLATFORM}").r0'
@@ -1671,16 +1710,18 @@ Section "Uninstall"
16711710
System::Call 'kernel32::SetEnvironmentVariable(t,t)i("INSTALLER_UNATTENDED", "0").r0'
16721711
${EndIf}
16731712

1674-
{%- if uninstall_with_conda_exe %}
16751713
${If} ${FileExists} "$INSTDIR\pkgs\pre_uninstall.bat"
16761714
${Print} "Running pre_uninstall scripts..."
16771715
push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\pre_uninstall.bat"'
16781716
push "Failed to run pre_uninstall"
16791717
push 'WithLog'
16801718
call un.AbortRetryNSExecWait
16811719
${EndIf}
1682-
!insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath"
1720+
${If} ${FileExists} "$INSTDIR\.nonadmin"
1721+
!insertmacro AddRemovePath "remove" "un."
1722+
${EndIf}
16831723

1724+
{%- if uninstall_with_conda_exe %}
16841725
# Parse arguments
16851726
StrCpy $R0 ""
16861727

@@ -1727,14 +1768,6 @@ Section "Uninstall"
17271768
call un.AbortRetryNSExecWait
17281769
SetDetailsPrint both
17291770
{%- endfor %}
1730-
${If} ${FileExists} "$INSTDIR\pkgs\pre_uninstall.bat"
1731-
${Print} "Running pre_uninstall scripts..."
1732-
push '"$CMD_EXE" /D /C "$INSTDIR\pkgs\pre_uninstall.bat"'
1733-
push "Failed to run pre_uninstall"
1734-
push 'WithLog'
1735-
call un.AbortRetryNSExecWait
1736-
${EndIf}
1737-
!insertmacro AbortRetryNSExecWaitLibNsisCmd "rmpath"
17381771
{%- if has_conda %}
17391772
${If} ${FileExists} "$INSTDIR\.nonadmin"
17401773
StrCpy $R0 "user"

constructor/osx/run_installation.sh

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,6 @@ find "$PREFIX/pkgs" -type d -empty -exec rmdir {} \; 2>/dev/null || :
119119
{{ condarc }}
120120
{%- endfor %}
121121

122-
if ! "$PREFIX/bin/python" -V; then
123-
echo "ERROR running Python"
124-
exit 1
125-
fi
126-
127122
# This is not needed for the default install to ~, but if the user changes the
128123
# install location, the permissions will default to root unless this is done.
129124
chown -R "${USER}" "$PREFIX"

constructor/winexe.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,27 @@ def make_nsi(
225225

226226
# From now on, the items added to variables will NOT be escaped
227227

228-
py_name, py_version, _ = filename_dist(dists[0]).rsplit("-", 2)
229-
assert py_name == "python"
230-
variables["pyver_components"] = py_version.split(".")
231-
232228
# These are mostly booleans we use with if-checks
229+
default_uninstall_name = "${NAME} ${VERSION}"
230+
variables["has_python"] = False
231+
for dist in dists:
232+
py_name, py_version, _ = filename_dist(dist).rsplit("-", 2)
233+
if py_name == "python":
234+
variables["has_python"] = True
235+
variables["pyver_components"] = py_version.split(".")
236+
break
237+
238+
if variables["has_python"]:
239+
variables["register_python"] = info.get("register_python", True)
240+
variables["register_python_default"] = info.get("register_python_default", None)
241+
default_uninstall_name += " (Python ${PYVERSION} ${ARCH})"
242+
else:
243+
variables["register_python"] = False
244+
variables["register_python_default"] = None
245+
233246
variables.update(ns_platform(info["_platform"]))
234247
variables["initialize_conda"] = info.get("initialize_conda", "classic")
235248
variables["initialize_by_default"] = info.get("initialize_by_default", None)
236-
variables["register_python"] = info.get("register_python", True)
237-
variables["register_python_default"] = info.get("register_python_default", None)
238249
variables["check_path_length"] = info.get("check_path_length", False)
239250
variables["check_path_spaces"] = info.get("check_path_spaces", True)
240251
variables["keep_pkgs"] = info.get("keep_pkgs") or False
@@ -247,6 +258,7 @@ def make_nsi(
247258
variables["custom_conclusion"] = info.get("conclusion_file", "").endswith(".nsi")
248259
variables["has_license"] = bool(info.get("license_file"))
249260
variables["uninstall_with_conda_exe"] = bool(info.get("uninstall_with_conda_exe"))
261+
variables["needs_python_exe"] = info.get("_win_install_needs_python_exe", True)
250262

251263
approx_pkgs_size_kb = approx_size_kb(info, "pkgs")
252264

@@ -259,9 +271,7 @@ def make_nsi(
259271
variables["SETUP_ENVS"] = setup_envs_commands(info, dir_path)
260272
variables["WRITE_CONDARC"] = list(add_condarc(info))
261273
variables["SIZE"] = approx_pkgs_size_kb
262-
variables["UNINSTALL_NAME"] = info.get(
263-
"uninstall_name", "${NAME} ${VERSION} (Python ${PYVERSION} ${ARCH})"
264-
)
274+
variables["UNINSTALL_NAME"] = info.get("uninstall_name", default_uninstall_name)
265275
variables["EXTRA_FILES"] = get_extra_files(extra_files, dir_path)
266276
variables["SCRIPT_ENV_VARIABLES"] = {
267277
key: win_str_esc(val) for key, val in info.get("script_env_variables", {}).items()

0 commit comments

Comments
 (0)