467 lines
15 KiB
Bash
467 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
unset CDPATH
|
|
|
|
if [ -d /C ]; then # Windows
|
|
home="$USERPROFILE" # NOTE: checked for existence later instead of here.
|
|
downloads="$home\\Downloads"
|
|
base="$home\\mamba"
|
|
else # Linux
|
|
home="$HOME" # NOTE: checked for existence later instead of here.
|
|
downloads="$home/src"
|
|
base="$home/mamba"
|
|
fi
|
|
|
|
# micromamba stuff; doesn't affect python itself:
|
|
installer_version="1.5.7" # remember to update both sha256s if you change this!
|
|
installer_archive="micromamba-$installer_version-0.tar.bz2"
|
|
|
|
releases="https://github.com/mamba-org/micromamba-releases/releases/download"
|
|
if [ -d /C ]; then # Windows
|
|
installer_sha256="c62a6a273e9a2b0313d2d6e50f27222447b3a2005dc4b9fef83ccd50b5ccce64"
|
|
installer_remote="$releases/$installer_version-0/micromamba-win-64.tar.bz2"
|
|
installer_path="Library/bin/micromamba.exe"
|
|
else # Linux
|
|
installer_sha256="2b92516aa575b42467805f25315bbd01454b8c7ec59ce04a5092187c4958c9a2"
|
|
installer_remote="$releases/$installer_version-0/micromamba-linux-64.tar.bz2"
|
|
installer_path="bin/micromamba"
|
|
fi
|
|
|
|
# remember not to use == when you mean = here:
|
|
python='python=3.11'
|
|
channel=conda-forge
|
|
|
|
main_packages=(
|
|
'pip>=23.3.1' # python package manager
|
|
)
|
|
|
|
# pip packages should be pinned with pure equality
|
|
# because they're not exported by mamba env export.
|
|
pypi_packages=(
|
|
'hipsterplot==0.1' # terminal plotting
|
|
)
|
|
|
|
# end basic settings, begin functions:
|
|
|
|
check_installer() {
|
|
[ -s "$1" ] || return
|
|
printf '%s %s\n' "$installer_sha256" "$1" | sha256sum -c - || return
|
|
}
|
|
|
|
mamba_paths() {
|
|
if [ -d /C ]; then # Windows
|
|
# TODO: infer System32 path by envvars. (it's looking for chcp.com)
|
|
printf %s \
|
|
"$(cygpath -u "C:\\Windows\\System32")" \
|
|
: \
|
|
"$(cygpath -u "C:\\Windows\\")" \
|
|
;
|
|
else # Linux
|
|
printf %s \
|
|
"/usr/local/sbin" \
|
|
: \
|
|
"/usr/local/bin" \
|
|
: \
|
|
"/usr/sbin" \
|
|
: \
|
|
"/usr/bin" \
|
|
: \
|
|
"/sbin" \
|
|
: \
|
|
"/bin" \
|
|
;
|
|
fi
|
|
}
|
|
|
|
python_paths() { # relies on globals: fullbase, env
|
|
if [ -d /C ]; then # Windows
|
|
printf %s \
|
|
"$(cygpath -u "$fullbase\\envs\\$env")" \
|
|
: \
|
|
"$(cygpath -u "$fullbase\\envs\\$env\\Library\\mingw-w64\\bin")" \
|
|
: \
|
|
"$(cygpath -u "$fullbase\\envs\\$env\\Library\\bin")" \
|
|
;
|
|
else # Linux
|
|
printf %s \
|
|
"$(readlink -f "$fullbase/envs/$env/x86_64-conda-linux-gnu/bin")" \
|
|
: \
|
|
"$(readlink -f "$fullbase/envs/$env/bin")" \
|
|
;
|
|
fi
|
|
printf %s "${MOREPATH:+:}$MOREPATH"
|
|
}
|
|
|
|
run_mamba() { # relies on globals: fullbase, base, env
|
|
local paths=
|
|
paths="$(mamba_paths)" || return
|
|
PATH="$paths" "$fullbase/$installer_path" --no-rc --no-env -r "$base" "$@"
|
|
}
|
|
|
|
run_python() { # relies on globals: fullbase, env
|
|
local exe= paths=
|
|
[ -d /C ] && exe=python.exe || exe=python
|
|
paths="$(python_paths)" || return
|
|
PATH="$paths" "$exe" "$@"
|
|
}
|
|
|
|
find_msvc() { # TODO: does this use any globals? besides SYSTEMDRIVE which is an envvar.
|
|
[ -d /C ] || { echo 'Internal error: attempted Windows routine on Linux!' >&2; exit 1; }
|
|
[ -n "$SYSTEMDRIVE" ] || { echo '$SYSTEMDRIVE must be set.' >&2; exit 1; }
|
|
|
|
dirs= libs= lib= includes=
|
|
local dir= temp=
|
|
local profiles="Program Files (x86)"
|
|
|
|
cd "$SYSTEMDRIVE" || return
|
|
[ -d "$profiles" ] || return
|
|
|
|
add_dir() {
|
|
dir="$1"
|
|
dir="${dir%/}"
|
|
dir="$(readlink -f "$dir")" || { printf 'readlink failed: %s\n' "$1" >&2; return 1; }
|
|
if [ ! -d "$dir" ] && [ -e "$dir" ]; then
|
|
dir="${dir%/*}"
|
|
fi
|
|
[ -d "$dir" ] || { printf 'not a directory: %s\n' "$dir"; return 1; }
|
|
dirs+=":$dir"
|
|
}
|
|
|
|
windir() {
|
|
# TODO: don't use cygpath, somehow.
|
|
temp="$(cygpath -w "$1")" || return
|
|
[ -d "$temp" ] || return
|
|
}
|
|
|
|
dir="$profiles/Microsoft Visual Studio"
|
|
[ -d "$dir" ] || return
|
|
dir="$(find "$dir" -name cl.exe | grep -E '[\/]Hostx64[\/]x64[\/]cl.exe$' | LC_ALL=C sort | tail -n1)"
|
|
add_dir "$dir" || return
|
|
|
|
windir "${dir%/bin/*}/lib/x64" || return; libs+=";$temp"; lib="$temp"
|
|
windir "${dir%/bin/*}/include" || return; includes+=";$temp"
|
|
|
|
dir="$profiles/Windows Kits"
|
|
[ -d "$dir" ] || return
|
|
dir="$(find "$dir" -name rc.exe | grep -E '[\/]x64[\/]rc.exe' | LC_ALL=C sort | tail -n1)"
|
|
add_dir "$dir" || return
|
|
|
|
# NOTE: this next line relies on add_dir behavior a little.
|
|
windir "$dir/ucrt" || return; includes+=";$temp"
|
|
|
|
dir="${dir%/x64}"
|
|
windir "${dir/bin/Lib}/um/x64" || return; libs+=";$temp"
|
|
windir "${dir/bin/Lib}/ucrt/x64" || return; libs+=";$temp"
|
|
windir "${dir/bin/Include}/ucrt" || return; includes+=";$temp"
|
|
windir "${dir/bin/Include}/shared" || return; includes+=";$temp"
|
|
|
|
dirs="${dirs#:}"
|
|
libs="${libs#;}"
|
|
includes="${includes#;}"
|
|
#printf '%s\n' "DIRS: $dirs" >&2
|
|
#printf '%s\n' "LIBS: $libs" >&2
|
|
#printf '%s\n' "INCLUDES: $includes" >&2
|
|
#exit 1
|
|
}
|
|
|
|
install_msvc() { # TODO: document globals used.
|
|
[ -d /C ] || { echo 'Internal error: attempted Windows routine on Linux!' >&2; exit 1; }
|
|
[ -n "$SYSTEMDRIVE" ] || { echo '$SYSTEMDRIVE must be set.' >&2; exit 1; }
|
|
[ -n "$COMSPEC" ] || { echo '$COMSPEC must be set.' >&2; exit 1; }
|
|
|
|
# find the latest version: curl -I 'https://aka.ms/vs/17/release/vs_buildtools.exe'
|
|
local uuid='5bebe58c-9308-4a5b-9696-b6f84e90a32e'
|
|
local hash='d62702bf9e2bb2c8be1f85ec4b86e0426e42646d12ac5196c451574d22be148e'
|
|
local exe='vs_BuildTools.exe'
|
|
local installer="$downloads\\$exe"
|
|
local url="https://download.visualstudio.microsoft.com/download/pr/$uuid/$hash/$exe"
|
|
local profiles="Program Files (x86)"
|
|
|
|
mode=install
|
|
if [ "$1" = update ]; then
|
|
shift
|
|
mode=update
|
|
fi
|
|
|
|
# always download the installer since it's only a couple megabytes.
|
|
curl -L "$url" -o "$installer" || return
|
|
printf '%s %s\n' "$hash" "$installer" | sha256sum -c - || return
|
|
|
|
cd "$SYSTEMDRIVE" || return
|
|
[ -d "$profiles" ] || return
|
|
|
|
local bt
|
|
bt="$profiles\\Microsoft Visual Studio\\2022\\BuildTools"
|
|
|
|
# TODO: use "update" instead of "install" when applicable.
|
|
# despite the name, you can use the Windows 11 SDK (or at least this version of it)
|
|
# to target as early as Windows 10 v1507.
|
|
"$installer" --quiet --wait --norestart --nocache "$mode" \
|
|
--add 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64' \
|
|
--add 'Microsoft.VisualStudio.Component.Windows11SDK.22000'
|
|
local ret=$?
|
|
|
|
# bash has run ERRORLEVEL through a modulus of 256, so some hacks are required.
|
|
local code=0
|
|
case $ret in
|
|
(0) code=0;; # success (probably)
|
|
(58) code=-1073741510;;
|
|
(65) code=8001;;
|
|
(66) code=1602;; # NOTE: this could also be 8002!
|
|
(67) code=8003;;
|
|
(68) code=8004;;
|
|
(69) code=8005;;
|
|
(70) code=8006;;
|
|
(82) code=1618;;
|
|
(87) code="INVALID ARGUMENTS ($ret)";;
|
|
(105) code=1641;;
|
|
(139) code=5003;;
|
|
(140) code=5004;;
|
|
(141) code=5005;;
|
|
(143) code=5007;;
|
|
(145) code=-1073720687;;
|
|
(194) code=3010;; # success; restart required (but not really)
|
|
(228) code=740;;
|
|
(233) code=1001;;
|
|
(235) code=1003;;
|
|
(*) code="UNKNOWN ($ret)";;
|
|
esac
|
|
|
|
printf '%s exited with %%ERRORLEVEL%% %s.\n' "$installer" "$code" >&2
|
|
[ "$code" = 0 ] || [ "$code" = 3010 ] || return 1
|
|
}
|
|
|
|
run_with_compiler() { # relies on globals: fullbase, env, working_dir
|
|
if [ -d /C ]; then # Windows
|
|
find_msvc || install_msvc \
|
|
|| { printf 'failed to install MSVC and a Windows SDK.\n' >&2; exit 1; }
|
|
find_msvc \
|
|
|| { printf 'failed to find a valid installation of MSVC.\n' >&2; exit 1; }
|
|
# globals should now exist: dirs, libs, lib, includes
|
|
|
|
(
|
|
cd "$working_dir" || return
|
|
export CMAKE_GENERATOR="Visual Studio 17 2022"
|
|
MOREPATH="$dirs" LIB="$libs" LIBPATH="$lib" INCLUDE="$includes" \
|
|
run_python "$@"
|
|
)
|
|
|
|
else # Linux
|
|
printf '%s\n' 'run_with_compiler: TODO: handle static gcc here!' >&2
|
|
run_python "$@"
|
|
fi
|
|
}
|
|
|
|
run_pip() {
|
|
run_with_compiler -m pip --disable-pip-version-check "$@"
|
|
}
|
|
|
|
find_last_env() { # relies on globals: fullbase
|
|
local arg="$1" e= oe=
|
|
env="$(date -u '+%Y%m%d')" || exit
|
|
n=1
|
|
while [ -d "$fullbase/envs/$env-$n" ]; do
|
|
let n++
|
|
done
|
|
new_env="$env-$n"
|
|
let n--
|
|
if [ $n = 0 ]; then
|
|
env=_
|
|
if [ -d "$fullbase/envs" ]; then
|
|
for e in "$fullbase/envs/"*; do
|
|
oe="${e##*/}"
|
|
#printf 'considering %s... compared to %s\n' "$oe" "$env" >&2
|
|
if [ -d "$e" ] && [ "${oe#20}" != "$oe" ] && [[ "$oe" > "$env" ]]; then
|
|
#printf '%s is more recent!\n' "$oe"
|
|
env="$oe"
|
|
fi
|
|
done
|
|
fi
|
|
if [ "$env" = _ ]; then
|
|
env=ERROR
|
|
if [ "$arg" != new ]; then
|
|
echo 'failed to find most recent environment' >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
else
|
|
env="$env-$n"
|
|
fi
|
|
}
|
|
|
|
# end of functions, begin main:
|
|
|
|
if [ -d /C ]; then # Windows
|
|
[ -n "$USERPROFILE" ] || { echo '$USERPROFILE must be set.' >&2; exit 1; }
|
|
else # Linux
|
|
[ -n "$HOME" ] || { echo '$HOME must be set.' >&2; exit 1; }
|
|
fi
|
|
|
|
[ -d "$downloads" ] || mkdir "$downloads" || exit
|
|
|
|
working_dir="$PWD"
|
|
[ -d "$working_dir" ] || { echo 'failed to determine working directory' >&2; exit 1; }
|
|
|
|
[ -d "$base" ] || mkdir "$base" || exit
|
|
if [ -d /C ]; then # Windows
|
|
fullbase="$(cygpath -u "$base")" || exit
|
|
fullbase="$(readlink -f "$fullbase")" || exit
|
|
fullbase="$(cygpath -w "$fullbase")" || exit
|
|
else # Linux
|
|
fullbase="$(readlink -f "$base")" || exit
|
|
fi
|
|
|
|
version="unknown"
|
|
if [ -s "$fullbase/$installer_path" ]; then
|
|
# TODO: do something meaningful if the execution itself fails.
|
|
version="$(run_mamba --version)"
|
|
fi
|
|
|
|
if [ "$version" != "$installer_version" ]; then
|
|
archive="$downloads/$installer_archive"
|
|
if check_installer "$archive"; then
|
|
:
|
|
else
|
|
# slightly less safe: curl -LOJ "$installer_remote"
|
|
curl -L "$installer_remote" -o "$archive" || exit
|
|
check_installer "$archive" || exit
|
|
fi
|
|
! [ -d /C ] || archive="$(cygpath -u "$archive")" || exit
|
|
cd "$fullbase" || exit
|
|
if ! which bzip2 >/dev/null 2>&1 && which python3 >/dev/null 2>&1; then
|
|
# ubuntu-server does not come with bzip2, but python3 can save us here.
|
|
# 'import tarfile,sys;_,a,b,c=sys.argv;tarfile.open(a,"r:bz2").extract(b,c)
|
|
python3 -c 'import tarfile,sys;_,a,b=sys.argv;tarfile.open(a,"r:bz2").extract(b)' \
|
|
"$archive" "$installer_path" || exit
|
|
else
|
|
tar jxf "$archive" "$installer_path" || exit
|
|
fi
|
|
cd "$working_dir" || exit
|
|
fi
|
|
|
|
[ "$1" = --create ] && creating=1 || creating=0
|
|
[ "$1" = --mamba ] && dancing=1 || dancing=0
|
|
[ "$1" = --pip ] && compiling=1 || compiling=0
|
|
[ "$1" = --wheel ] && wheeling=1 || wheeling=0
|
|
[ "$1" = --msvc ] && visualizing=1 || visualizing=0
|
|
|
|
if [ "${AM_ENV:-__empty__}" = __empty__ ]; then
|
|
find_last_env new || exit
|
|
else
|
|
if [ $creating = 1 ]; then
|
|
:
|
|
elif [ ! -d "$fullbase/envs/$AM_ENV" ]; then
|
|
printf 'failed to find environment $AM_ENV: %s\n' "$AM_ENV" >&2
|
|
sleep 3 #exit 1
|
|
elif [ ! -s "$fullbase/envs/$AM_ENV/LICENSE_PYTHON.txt" ]; then
|
|
printf 'empty environment $AM_ENV: %s\n' "$AM_ENV" >&2
|
|
sleep 3 #exit 1
|
|
fi
|
|
env="$AM_ENV"
|
|
fi
|
|
|
|
if [ $visualizing = 1 ]; then
|
|
[ $visualizing = 0 ] || shift || exit
|
|
install_msvc update
|
|
exit
|
|
fi
|
|
|
|
if [ $dancing = 1 ]; then
|
|
[ $dancing = 0 ] || shift || exit
|
|
|
|
no_default=0
|
|
for arg in "$@"; do
|
|
[ "$arg" != --name ] || no_default=1
|
|
# TODO: is --name=stuff valid? detect that too.
|
|
if [ "${arg#--}" = "$arg" ]; then # ignore other long arguments
|
|
# detect -n, -asdfn shortargs. startswith -, endswith n.
|
|
if [ "${arg#-}" != "$arg" ] && [ "${arg%n}" != "${arg}" ]; then
|
|
no_default=1
|
|
fi
|
|
fi
|
|
done
|
|
if [ $no_default = 0 ]; then
|
|
run_mamba -n "$env" "$@" || exit
|
|
else
|
|
run_mamba "$@" || exit
|
|
fi
|
|
|
|
[ $dancing = 0 ] || exit 0
|
|
fi
|
|
|
|
if [ $compiling = 1 ]; then
|
|
[ $compiling = 0 ] || shift || exit
|
|
|
|
[ "$env" != ERROR ] || { printf 'failed to find most recent environment\n' >&2; exit 1; }
|
|
run_pip "$@" || exit
|
|
|
|
[ $compiling = 0 ] || exit 0
|
|
fi
|
|
|
|
if [ $wheeling = 1 ]; then
|
|
[ $wheeling = 0 ] || shift || exit
|
|
|
|
[ "$env" != ERROR ] || { printf 'failed to find most recent environment\n' >&2; exit 1; }
|
|
run_with_compiler -u setup.py bdist_wheel || exit
|
|
|
|
[ $wheeling = 0 ] || exit 0
|
|
fi
|
|
|
|
if [ "$env" = ERROR ] || [ $creating != 0 ]; then
|
|
[ $creating = 0 ] || shift || exit
|
|
[ -z "$new_env" ] || env="$new_env"
|
|
|
|
if [ $creating = 0 ]; then
|
|
printf 'WARNING: automatically creating a new environment: %s\n' "$env" >&2
|
|
fi
|
|
|
|
_do_create() {
|
|
if [ $# = 0 ]; then
|
|
run_mamba -yqn "$env" create -c "$channel" "$python" "${main_packages[@]}"
|
|
else
|
|
run_mamba -yqn "$env" create "$@"
|
|
fi
|
|
}
|
|
|
|
if [ $creating = 1 ]; then
|
|
_do_create "$@" 2>&1 | tee "$fullbase/create.log"
|
|
else
|
|
_do_create 2>&1 | tee "$fullbase/create.log"
|
|
fi
|
|
res=${PIPESTATUS[0]}
|
|
[ $res = 0 ] || exit "$res"
|
|
|
|
if [ $# = 0 ]; then
|
|
common=(--isolated --disable-pip-version-check)
|
|
run_pip install "${common[@]}" --no-deps "${pypi_packages[@]}" 2>&1 \
|
|
| tee -a "$fullbase/create.log"
|
|
[ ${PIPESTATUS[0]} = 0 ] || exit ${PIPESTATUS[0]}
|
|
# using run_python is faster than run_pip since it won't check for compilers.
|
|
run_python -m pip "${common[@]}" check \
|
|
| tee -a "$fullbase/create.log"
|
|
#[ ${PIPESTATUS[0]} = 0 ] || exit ${PIPESTATUS[0]}
|
|
fi
|
|
|
|
pre="$fullbase/$env"
|
|
|
|
run_mamba -n "$env" env export > "$pre.yml" || exit
|
|
|
|
run_mamba -n "$env" list --json | run_python -c \
|
|
'for(p)in(m:=__import__)("json").load(m("sys").stdin):print(p["name"]+"=="+p["version"])' \
|
|
> "$pre.conda.freeze.txt" || exit
|
|
[ ${PIPESTATUS[0]} = 0 ] || exit ${PIPESTATUS[0]}
|
|
|
|
run_python -m pip "${common[@]}" list --pre --format=freeze \
|
|
> "$pre.pip.freeze.txt" || exit
|
|
|
|
echo '- pip:' | cat "$pre.yml" - > "$pre.with-pip.yml" || exit
|
|
run_python -c \
|
|
'[print(*" -",b[k])for(a,b)in[map(lambda f:{l.strip().lower().replace(*"-_"):l.strip()for(l)in open(f)},__import__("sys").argv[1:])]for(k)in(b)if(k)not in a]' \
|
|
"$pre.conda.freeze.txt" "$pre.pip.freeze.txt" \
|
|
>> "$pre.with-pip.yml" || exit
|
|
|
|
rm "$pre.conda.freeze.txt" "$pre.pip.freeze.txt" || exit
|
|
|
|
[ $creating = 0 ] || exit 0
|
|
fi
|
|
|
|
run_python "$@"
|