#!/bin/bash # Install a copy of the shared bernina pixi python environment into an # experiment's pgroup work folder (or any folder), and make it visible as # a kernel in JupyterHub. # # Copies SOURCE_DIR's pixi.toml/pixi.lock straight into the target folder # (no python/ subfolder) and runs `pixi install --frozen` there, i.e. # exactly what is pinned in the lock -- nothing gets re-solved. Once the # pgroup's copy has local changes (packages added with -a/-A or `pixi # add`), re-runs keep them; only -u resets to the shared files. # Only one environment is installed: the one SOURCE_DIR's bpy-default # link points to, unless -e picks another. # # Target folder: # (no argument) the current folder # pNNNNN /sf/bernina/data/pNNNNN/work # the pgroup behind /sf/bernina/exp/ (same # lookup as name2pgroups; exact match wins, otherwise the # first substring match), then its work folder # any folder given as a path (must contain a "/") # pgroup work folders live on /das/work, which is only mounted on ra -- # run this there. # # Package cache: pixi hardlinks packages from its cache into the env when # both are on the same filesystem, so by default the cache goes into # /pixi_cache (also keeps multi-GB downloads out of $HOME). An # already set PIXI_CACHE_DIR or RATTLER_CACHE_DIR pointing at an existing, # writable folder is respected instead. # # JupyterHub (nb_conda_kernels) lists every env found in ~/.condarc's # envs_dirs as "Python [conda env:]". is the env folder's # basename -- unless its parent folder is literally called "envs", in # which case the grandparent's name is prepended (that's where the hub's # "bm-bernina310" comes from, and why .pixi/envs/bpy312 would show up as # ".pixi-bpy312"). So the env gets a symlink # /conda_envs/bernina-- -> ../.pixi/envs/ # (plus bernina--default for the default env). conda_envs -- # deliberately NOT named "envs" -- is what each user who wants the kernels # adds to envs_dirs in their own ~/.condarc, once (-J; the install prints # that command ready to pass on). # Symlinks are not resolved for naming (cf. bpy-default in the hub). # On ra, ~ is /das/home/$USER, which is the .condarc the hub reads. set -euo pipefail SOURCE_DIR="/sf/bernina/applications/python" PIXI_BIN="/usr/bin/pixi" # console (system) pixi first [ -x "$PIXI_BIN" ] || PIXI_BIN="/sf/bernina/bin/pixi" # fallback, e.g. on ra (RHEL8) EXP_DIR="/sf/bernina/exp" DATA_DIR="/sf/bernina/data" LINK_DIR_NAME="conda_envs" # must not be "envs", see above CACHE_DIR_NAME="pixi_cache" SELF_PATH="/sf/bernina/bin/create-python-install" # full path: bin is not on everyone's PATH on ra ENV_NAME="" PREFIX="" JOIN_ONLY=0 ASSUME_YES=0 DRY_RUN=0 RESET=0 PERMS_ONLY=0 ADD_CONDA=() ADD_PYPI=() usage() { cat < (as in name2pgroups). A part of the name is enough, e.g. "blanco" for 26i_blanco; an exact name wins, otherwise the first match alphabetically is used (others listed). path any folder, given as a path (must contain a "/") On the first run, pixi.toml and pixi.lock are copied directly into that folder (no subfolder). 'pixi install --frozen' then installs exactly what the lock pins -- nothing is re-solved or updated. The env ends up in /.pixi/envs/. Re-runs never throw away the pgroup's own changes: if the folder's pixi.toml/pixi.lock differ from the shared ones (packages added with -a/-A or 'pixi add'), they are kept and installed as they are. Only -u resets them to the shared files (the old ones are kept as pixi.toml. / pixi.lock.). WHICH ENVIRONMENT One env only. Default: the bernina default, i.e. what $SOURCE_DIR/.pixi/envs/bpy-default points to (currently: ${DEFAULT_ENV_HINT:-?}). Pick another with -e, e.g. -e bpy313. Available: ${ENVS_HINT:-see [environments] in pixi.toml} PACKAGE CACHE If PIXI_CACHE_DIR or RATTLER_CACHE_DIR is set to an existing, writable folder, that cache is used. Otherwise: /$CACHE_DIR_NAME (same filesystem as the env, so packages are hardlinked, not copied, and the multi-GB download stays out of \$HOME). That one is shared by the whole pgroup. For plain pixi commands later, point pixi at it yourself: export PIXI_CACHE_DIR=/$CACHE_DIR_NAME SHARING WITHIN THE PGROUP .pixi/, $LINK_DIR_NAME/ and $CACHE_DIR_NAME/ are made group-writable (setgid folders + a default ACL, group rw files), so every pgroup member can use the cache, update the env or install into it. Only the owner of a file can change its permissions: each member who runs the script fixes their own. Use umask 002 for plain pixi commands (and don't run two installs into the same env at once). -P fixes up your own files without installing anything, e.g. after plain pixi commands. ADDING PACKAGES -a pkg (conda-forge) / -A pkg (PyPI), repeatable, version specs allowed: $(basename "$0") -a scikit-image -A "somepkg>=1.2" 26i_blanco This does 'pixi add' into the pgroup's pixi.toml (recorded in its pixi.lock, so it survives re-installs), installs, and fixes permissions -- with umask 002 and the pgroup cache set. The package goes into the part of pixi.toml that every env shares, so all envs' locks are re-solved (versions only change where they must); only the selected env is installed. A quick 'pip install' in the env also works, but isn't recorded and can disappear on the next install. JUPYTERHUB The hub lists every conda env it finds in the folders named under envs_dirs in ~/.condarc, as "Python [conda env:]". So the script creates named symlinks /$LINK_DIR_NAME/bernina-- -> ../.pixi/envs/ /$LINK_DIR_NAME/bernina--default (only for the default env) E.g. for 26i_blanco the hub shows: Python [conda env:bernina-26i-blanco-bpy312] Python [conda env:bernina-26i-blanco-default] The name is only the symlink's name: rename/add a link in $LINK_DIR_NAME/ to rename it, no reinstall needed. (The folder must not be called "envs" -- the hub would then prepend the parent folder's name.) To see them, everyone (you too) adds /$LINK_DIR_NAME to envs_dirs in their own ~/.condarc once -- on ra, since that's the ~/.condarc the hub reads: $(basename "$0") -J 26i_blanco Nothing else is installed or changed by -J. Every install prints this command for its pgroup, ready to pass on to collaborators. OPTIONS -e env install this environment instead of the default -p prefix jupyter name prefix instead of bernina- (underscores in exp names become dashes) -a pkg add a conda-forge package (repeatable), see ADDING PACKAGES -A pkg add a PyPI package (repeatable) -u reset the folder's pixi.toml/pixi.lock to the shared ones (removes locally added packages; backups are kept) -P only make your own files in the install group-writable -J only add the folder's kernels to your ~/.condarc (JupyterHub) -y don't ask for confirmation -n dry run: show what would be done, change nothing -m short manual (markdown, rendered with glow if available) -h this help EXAMPLES $(basename "$0") -n 26i_blanco # see what would happen $(basename "$0") 26i_blanco # bpy312 into p23620's work folder $(basename "$0") -e bpy313 p23620 # a different env $(basename "$0") -a scikit-image 26i_blanco # add a package $(basename "$0") -u 26i_blanco # back to the shared env cd /sf/bernina/data/p23620/work && $(basename "$0") AFTERWARDS Activate in a shell: eval "\$($PIXI_BIN shell-hook -e --manifest-path /pixi.toml)" Re-running it is always safe (joining colleagues do exactly that); -u brings the copy back in line with the shared pixi.lock. EOF exit 0 } die() { echo "error: $*" >&2; exit 1; } run() { if [ "$DRY_RUN" -eq 1 ]; then echo " [dry-run] $*"; else "$@"; fi; } bad_usage() { echo "error: $*" >&2; echo "see: $(basename "$0") -h" >&2; exit 1; } # for the help text / manual DEFAULT_ENV_HINT=$(basename "$(readlink "$SOURCE_DIR/.pixi/envs/bpy-default" 2>/dev/null)" 2>/dev/null || true) ENVS_HINT=$(awk '/^\[/{s=($0=="[environments]")} s && /^[A-Za-z0-9_-]+[[:space:]]*=/{sub(/[[:space:]]*=.*/,""); printf "%s ", $0}' "$SOURCE_DIR/pixi.toml" 2>/dev/null || true) # brief manual as markdown; rendered with glow on a terminal (-m) manual() { local md md=$(cat <<'EOF' # create-python-install **The bernina python environment in your pgroup, and in JupyterHub.** ## What it does 1. Copies the shared `pixi.toml` + `pixi.lock` from `/sf/bernina/applications/python` into the pgroup's **work** folder. 2. Installs **one** environment there, exactly as pinned in the shared lock (default: **@DEFAULT_ENV@**, the bernina default; others with `-e`). 3. Makes it show up in **JupyterHub** as `bernina--`. ## Quick start (on ra) ```bash create-python-install -n 26i_blanco # preview, changes nothing create-python-install 26i_blanco # install create-python-install -J 26i_blanco # kernels into your JupyterHub ``` Then reload JupyterHub and pick the kernel **Python [conda env:bernina-26i-blanco-@DEFAULT_ENV@]**. ## Where it installs | you give | installs into | |-------------------|----------------------------------------------| | nothing | the current folder | | `p23620` | `/sf/bernina/data/p23620/work` | | `26i_blanco` | that experiment's pgroup work folder | | `blanco` | same (part of the exp name is enough) | | `/some/path` | that folder | ## What ends up in the work folder ``` pixi.toml, pixi.lock copies of the shared ones .pixi/envs// the environment itself conda_envs/ named links JupyterHub picks up bernina-26i-blanco-bpy312 -> ../.pixi/envs/bpy312 bernina-26i-blanco-default -> ../.pixi/envs/bpy312 pixi_cache/ downloaded packages (unless you set your own) ``` Nothing in anyone's home. Whoever wants the kernels (you too) runs `-J` once: it adds one line under `envs_dirs` in their `~/.condarc`, pointing at `conda_envs/`. ## Good to know - **Run it on ra**: `/das/work` (the pgroup work folders) is only mounted there, and JupyterHub reads ra's `~/.condarc`. - **Kernel name** = the link's name in `conda_envs/`. Rename the link to rename the kernel; no reinstall needed. - **Cache**: your `PIXI_CACHE_DIR` is used if set, otherwise `pixi_cache/` in the work folder (keeps GBs out of your home), shared by the pgroup. - **Colleagues**: env, links and cache are group-writable, so any pgroup member can update or install into it. To see the kernels in *their* JupyterHub they run `create-python-install -J 26i_blanco` once (every install prints this command for its pgroup). - **Add a package**: `-a scikit-image` (conda-forge) or `-A somepkg` (PyPI). Recorded in the pgroup's own `pixi.toml`/`pixi.lock`, so re-runs keep it. - **Back to the shared env** (drops added packages, keeps backups): `-u`. - **Only fix permissions** of your files (e.g. after plain `pixi` commands): `-P`. - **Remove**: delete `.pixi/ conda_envs/ pixi_cache/ pixi.toml pixi.lock` and the `conda_envs` line in `~/.condarc`. Options and details: `create-python-install -h` EOF ) md="${md//@DEFAULT_ENV@/${DEFAULT_ENV_HINT:-bpy312}}" local glow glow=$(command -v glow || true) [ -n "$glow" ] || { [ -x /sf/daq/bin/glow ] && glow=/sf/daq/bin/glow; } if [ -t 1 ] && [ -n "$glow" ]; then printf '%s\n' "$md" | "$glow" - else printf '%s\n' "$md" fi exit 0 } while getopts "e:p:a:A:uPJynmh" opt; do case "$opt" in e) ENV_NAME="$OPTARG" ;; p) PREFIX="$OPTARG" ;; a) ADD_CONDA+=("$OPTARG") ;; A) ADD_PYPI+=("$OPTARG") ;; u) RESET=1 ;; P) PERMS_ONLY=1 ;; J) JOIN_ONLY=1 ;; y) ASSUME_YES=1 ;; n) DRY_RUN=1 ;; m) manual ;; h) usage ;; *) bad_usage "unknown option" ;; esac done shift $((OPTIND - 1)) [ $# -le 1 ] || bad_usage "only one target (pgroup, exp name or path) allowed" ARG="${1:-}" command -v "$PIXI_BIN" >/dev/null 2>&1 || PIXI_BIN="pixi" command -v "$PIXI_BIN" >/dev/null 2>&1 || die "pixi not found" [ -r "$SOURCE_DIR/pixi.toml" ] && [ -r "$SOURCE_DIR/pixi.lock" ] \ || die "can't read pixi.toml/pixi.lock in $SOURCE_DIR" # exp name -> exp link (same matching as name2pgroups: substring of the # link name; an exact name wins, otherwise the first one alphabetically) exp_link_for_name() { local name="$1" l matches=() if [ -L "$EXP_DIR/$name" ]; then echo "$EXP_DIR/$name"; return; fi for l in "$EXP_DIR"/*"$name"*; do [ -L "$l" ] && matches+=("$l") done [ ${#matches[@]} -gt 0 ] || return 1 if [ ${#matches[@]} -gt 1 ]; then echo "note: '$name' matches several experiments, using the first:" >&2 for l in "${matches[@]}"; do echo " $(basename "$l") -> $(basename "$(readlink -f "$l")")" >&2 done fi echo "${matches[0]}" } # pgroup -> exp name (like pgroup2name; first alphabetically if several) exp_name_for_pgroup() { local pgroup="$1" l for l in "$EXP_DIR"/*; do [ -L "$l" ] && [ "$(basename "$(readlink -f "$l")")" = "$pgroup" ] \ && { basename "$l"; return; } done return 1 } EXP_NAME="" PGROUP="" if [ -z "$ARG" ]; then TARGET="$PWD" elif [[ "$ARG" =~ ^p[0-9]{5}$ ]]; then PGROUP="$ARG" TARGET="$DATA_DIR/$PGROUP/work" elif [[ "$ARG" == */* ]]; then TARGET="$ARG" else link=$(exp_link_for_name "$ARG") || die "no experiment matching '$ARG' in $EXP_DIR" EXP_NAME=$(basename "$link") PGROUP=$(basename "$(readlink -f "$link")") TARGET="$DATA_DIR/$PGROUP/work" fi # resolve work -> /das/work/pNN/pNNNNN: env paths get baked into the # installed files, so use the real location, not the symlink TARGET_REAL=$(readlink -f "$TARGET" 2>/dev/null || true) if [ -z "$TARGET_REAL" ] || [ ! -d "$TARGET_REAL" ]; then die "target folder $TARGET doesn't exist${TARGET_REAL:+ (-> $TARGET_REAL)}; pgroup work folders (/das/work) are only mounted on ra" fi TARGET="$TARGET_REAL" [ "$TARGET" != "$(readlink -f "$SOURCE_DIR")" ] || die "target is the shared source project itself" [ -w "$TARGET" ] || die "no write permission in $TARGET" if [ -z "$PGROUP" ] && [[ "$TARGET" =~ /(p[0-9]{5})(/|$) ]]; then PGROUP="${BASH_REMATCH[1]}" fi if [ -z "$EXP_NAME" ] && [ -n "$PGROUP" ]; then EXP_NAME=$(exp_name_for_pgroup "$PGROUP" || true) fi if [ -z "$PREFIX" ]; then label="${EXP_NAME:-${PGROUP:-$(basename "$TARGET")}}" PREFIX="bernina-${label//_/-}" fi # Which pixi.toml/pixi.lock to install from: # fresh - none in the folder yet: copy the shared ones # shared - identical to the shared ones: nothing to copy # local - they differ (packages added in this pgroup): keep them # reset - they differ, but -u: back up and copy the shared ones if [ ! -e "$TARGET/pixi.toml" ] && [ ! -e "$TARGET/pixi.lock" ]; then SETUP=fresh elif cmp -s "$SOURCE_DIR/pixi.toml" "$TARGET/pixi.toml" \ && cmp -s "$SOURCE_DIR/pixi.lock" "$TARGET/pixi.lock"; then SETUP=shared elif [ "$RESET" -eq 1 ]; then SETUP=reset else SETUP=local [ -f "$TARGET/pixi.toml" ] && [ -f "$TARGET/pixi.lock" ] \ || die "$TARGET has only one of pixi.toml/pixi.lock; -u resets both to the shared ones" fi MANIFEST_SRC="$SOURCE_DIR/pixi.toml" [ "$SETUP" = local ] && MANIFEST_SRC="$TARGET/pixi.toml" DEFAULT_ENV=$(basename "$(readlink "$SOURCE_DIR/.pixi/envs/bpy-default" 2>/dev/null)" 2>/dev/null || true) [ -n "$DEFAULT_ENV" ] || die "can't resolve $SOURCE_DIR/.pixi/envs/bpy-default" ENV_NAME="${ENV_NAME:-$DEFAULT_ENV}" [ "$ENV_NAME" != "default" ] \ || die "pixi's own 'default' env isn't meant for this; the bernina default is $DEFAULT_ENV (used when -e is omitted)" defined_envs=$(awk '/^\[/{s=($0=="[environments]")} s && /^[A-Za-z0-9_-]+[[:space:]]*=/{sub(/[[:space:]]*=.*/,""); print}' "$MANIFEST_SRC") grep -qxF -- "$ENV_NAME" <<<"$defined_envs" \ || die "no environment '$ENV_NAME' in $MANIFEST_SRC (have: $(echo $defined_envs))" # package cache: keep a sensible user setting, otherwise the target folder cache_ok() { [ -n "$1" ] && [ -d "$1" ] && [ -w "$1" ]; } if cache_ok "${PIXI_CACHE_DIR:-}"; then CACHE_NOTE="(your PIXI_CACHE_DIR)" elif cache_ok "${RATTLER_CACHE_DIR:-}"; then export PIXI_CACHE_DIR="$RATTLER_CACHE_DIR" CACHE_NOTE="(your RATTLER_CACHE_DIR)" else export PIXI_CACHE_DIR="$TARGET/$CACHE_DIR_NAME" CACHE_NOTE="" fi LINK_DIR="$TARGET/$LINK_DIR_NAME" # only the pgroup's own cache is shared; a personal PIXI_CACHE_DIR is left alone SHARED=("$TARGET/.pixi" "$LINK_DIR") [ "$PIXI_CACHE_DIR" != "$TARGET/$CACHE_DIR_NAME" ] || SHARED+=("$PIXI_CACHE_DIR") LINKS=("$PREFIX-$ENV_NAME") [ "$ENV_NAME" = "$DEFAULT_ENV" ] && LINKS+=("$PREFIX-default") if [ "$PERMS_ONLY" -eq 0 ] && [ "$JOIN_ONLY" -eq 0 ]; then cat </dev/null 2>&1 && HAVE_SETFACL=1 share_dir() { # just this folder, not recursive [ -d "$1" ] && [ -O "$1" ] || return 0 chmod g+rwxs "$1" 2>/dev/null || true [ "$HAVE_SETFACL" -eq 0 ] || setfacl -m d:g::rwx,d:m::rwx "$1" 2>/dev/null || true } share_tree() { # everything below, owned by this user local d="$1" [ -d "$d" ] || return 0 find "$d" -user "$USER" -type d ! -perm -g+rwxs -exec chmod g+rwxs {} + 2>/dev/null || true find "$d" -user "$USER" -type f ! -perm -g+rw -exec chmod g+rw {} + 2>/dev/null || true [ "$HAVE_SETFACL" -eq 0 ] \ || find "$d" -user "$USER" -type d -exec setfacl -m d:g::rwx,d:m::rwx {} + 2>/dev/null || true } # append (not prepend: the first envs_dirs entry is where `conda create -n` # puts new envs) LINK_DIR to ~/.condarc; written with a redirect rather # than a rename, so a ~/.condarc symlink stays a symlink add_to_condarc() { local rc="$HOME/.condarc" dir="$1" new if [ -f "$rc" ]; then if awk -v d="$dir" '{sub(/[[:space:]]+$/,"")} $0 ~ /^[[:space:]]*-[[:space:]]/ {sub(/^[[:space:]]*-[[:space:]]*/,""); if ($0==d || $0==d"/") f=1} END{exit !f}' "$rc"; then echo "==> $dir already in $rc" return fi grep -qE '^envs_dirs:[[:space:]]*[^[:space:]#]' "$rc" \ && { echo "warning: envs_dirs in $rc isn't a block list, add $dir by hand" >&2; return; } new=$(awk -v d="$dir" ' /^envs_dirs:[[:space:]]*$/ { print; inblk = 1; next } inblk && /^[[:space:]]*-/ { print; next } inblk { print " - " d; inblk = 0; done = 1 } { print } END { if (inblk) { print " - " d; done = 1 } if (!done) { print "envs_dirs:"; print " - " d } }' "$rc") else new=$(printf 'envs_dirs:\n - %s' "$dir") fi if [ "$DRY_RUN" -eq 1 ]; then echo " [dry-run] $rc would become:"; sed 's/^/ /' <<<"$new" else printf '%s\n' "$new" > "$rc" echo "==> added $dir to $rc" fi } report_others() { local others others=$(find "${SHARED[@]}" ! -user "$USER" -type f ! -perm -g+w -printf '%u\n' 2>/dev/null | sort -u | tr '\n' ' ') [ -z "$others" ] || echo "note: some files of $others aren't group-writable yet;" \ "they can fix that with: $(basename "$0") -P $TARGET" } if [ "$PERMS_ONLY" -eq 1 ]; then [ -d "$TARGET/.pixi" ] || die "no pixi install in $TARGET" [ "$DRY_RUN" -eq 0 ] || { echo " [dry-run] make your files group-writable in: ${SHARED[*]}"; exit 0; } echo "==> making your files in $TARGET group-writable for $(stat -c %G "$TARGET")" for d in "${SHARED[@]}"; do share_tree "$d"; done report_others exit 0 fi # what collaborators (and you) run once to get the kernels in JupyterHub JOIN_ARG="${EXP_NAME:-${PGROUP:-$TARGET}}" JOIN_CMD="$SELF_PATH -J $JOIN_ARG" if [ "$JOIN_ONLY" -eq 1 ]; then kernels=$(ls "$LINK_DIR" 2>/dev/null || true) [ -n "$kernels" ] || die "no kernels in $LINK_DIR yet -- install first" case "$HOME" in /das/home/*) ;; *) echo "note: JupyterHub reads the ~/.condarc on ra (/das/home/$USER);" \ "this is $(hostname -s) with HOME=$HOME" >&2 ;; esac add_to_condarc "$LINK_DIR" echo "JupyterHub kernel(s) (reload the hub page):" for l in $kernels; do echo " Python [conda env:$l]"; done exit 0 fi # an active bpy-env.sh / pixi shell exports these and they would point pixi # at the shared project -- always address the target explicitly instead unset PIXI_PROJECT_MANIFEST PIXI_PROJECT_ROOT PIXI_IN_SHELL stamp=$(date +%Y%m%d-%H%M%S) case "$SETUP" in fresh|reset) for f in pixi.toml pixi.lock; do if [ -f "$TARGET/$f" ] && ! cmp -s "$SOURCE_DIR/$f" "$TARGET/$f"; then echo "==> keeping the pgroup's $f as $f.$stamp" run cp -p "$TARGET/$f" "$TARGET/$f.$stamp" fi run cp "$SOURCE_DIR/$f" "$TARGET/$f" done ;; local) echo "==> using the pgroup's own pixi.toml/pixi.lock (differ from the shared ones; -u resets)" ;; esac # (fails harmlessly if another member owns them -- then they're theirs to share) [ "$DRY_RUN" -eq 1 ] || chmod 664 "$TARGET/pixi.toml" "$TARGET/pixi.lock" 2>/dev/null || true # never let a global detached-environments setting move the envs away # from .pixi/envs, the jupyter links below rely on that location if [ ! -f "$TARGET/.pixi/config.toml" ]; then run mkdir -p "$TARGET/.pixi" if [ "$DRY_RUN" -eq 0 ]; then echo "detached-environments = false" > "$TARGET/.pixi/config.toml" else echo " [dry-run] write $TARGET/.pixi/config.toml" fi fi [ "$PIXI_CACHE_DIR" != "$TARGET/$CACHE_DIR_NAME" ] || run mkdir -p "$PIXI_CACHE_DIR" run mkdir -p "$TARGET/.pixi/envs" "$LINK_DIR" if [ "$DRY_RUN" -eq 0 ]; then # before installing, so everything pixi creates below inherits it for d in "${SHARED[@]}" "$TARGET/.pixi/envs"; do share_dir "$d"; done else echo " [dry-run] make group-writable: ${SHARED[*]}" fi # -a/-A: record the packages in pixi.toml/pixi.lock (re-solves the lock), # the install below then puts them into the env if [ ${#ADD_CONDA[@]} -gt 0 ]; then echo "==> adding ${ADD_CONDA[*]}" run "$PIXI_BIN" add --no-install --manifest-path "$TARGET/pixi.toml" "${ADD_CONDA[@]}" fi if [ ${#ADD_PYPI[@]} -gt 0 ]; then echo "==> adding (PyPI) ${ADD_PYPI[*]}" run "$PIXI_BIN" add --no-install --pypi --manifest-path "$TARGET/pixi.toml" "${ADD_PYPI[@]}" fi echo "==> installing $ENV_NAME" run "$PIXI_BIN" install --frozen -e "$ENV_NAME" --manifest-path "$TARGET/pixi.toml" for l in "${LINKS[@]}"; do run ln -sfn "../.pixi/envs/$ENV_NAME" "$LINK_DIR/$l" done if [ "$DRY_RUN" -eq 0 ]; then echo "==> making it group-writable for $(stat -c %G "$TARGET")" for d in "${SHARED[@]}"; do share_tree "$d"; done report_others fi [ "$DRY_RUN" -eq 0 ] || { echo; echo "Dry run, nothing changed."; exit 0; } cat <