@@ -127,6 +127,7 @@ show_help() {
127127 echo " "
128128 echo " Environment variables:"
129129 echo " VENV Virtual environment name, overrides --venv (default: .venv)"
130+ echo " TORCH_GROUP Torch dependency group to pin; source of truth for torch pinning"
130131 echo " "
131132 echo " Available dependency groups: $AVAILABLE_GROUPS "
132133 echo " Conflicting groups (excluded from --all-groups): ${CONFLICTING_GROUPS[*]} "
@@ -135,8 +136,8 @@ show_help() {
135136 echo " $0 --python-version 3.11 # Setup with dev group only"
136137 echo " $0 --python-version 3.11 --with-docs # Setup with dev and docs groups"
137138 echo " $0 --python-version 3.11 --all-groups # Setup with all non-conflicting groups"
138- echo " $0 --python-version 3.11 --all-groups --with-torch_2_11 # Setup with all groups and torch 2.11"
139- echo " $0 --python-version 3.11 --all-groups --with-torch_2_8 # Setup with all groups and torch 2.8"
139+ echo " TORCH_GROUP=torch_2_11 $0 --python-version 3.11 --all-groups # Setup with all groups and torch 2.11"
140+ echo " TORCH_GROUP=torch_2_8 $0 --python-version 3.11 --all-groups # Setup with all groups and torch 2.8"
140141 echo " $0 --python-version 3.11 --venv .venv-exp # Setup with custom venv name"
141142 echo " $0 --python-version 3.11 --with-docs --venv .venv-exp # Setup with docs group and custom venv name"
142143 echo " $0 --python-version 3.12 # Setup with Python 3.12"
@@ -222,6 +223,7 @@ validate_groups() {
222223# Validate dependency group names early
223224[[ ${# EXTRA_GROUPS[@]} -gt 0 ]] && validate_groups " ${EXTRA_GROUPS[@]} "
224225[[ ${# EXCLUDE_GROUPS[@]} -gt 0 ]] && validate_groups " ${EXCLUDE_GROUPS[@]} "
226+ [[ -n " ${TORCH_GROUP:- } " ]] && validate_groups " $TORCH_GROUP "
225227
226228# Check for conflicts between --with-<group> and --without-<group>
227229if [[ ${# EXTRA_GROUPS[@]} -gt 0 && ${# EXCLUDE_GROUPS[@]} -gt 0 ]]; then
@@ -233,6 +235,21 @@ if [[ ${#EXTRA_GROUPS[@]} -gt 0 && ${#EXCLUDE_GROUPS[@]} -gt 0 ]]; then
233235 done
234236fi
235237
238+ # TORCH_GROUP (env var) is the single source of truth for torch pinning; a
239+ # --with-<torch_2_*> flag that disagrees with it is almost certainly a
240+ # mistake (e.g. TORCH_GROUP left set from a prior `make` invocation) rather
241+ # than an intentional double-specification, so fail loudly instead of
242+ # leaving it to a raw `uv` conflicting-groups resolver error.
243+ if [[ -n " ${TORCH_GROUP:- } " ]]; then
244+ for GROUP in " ${EXTRA_GROUPS[@]:- } " ; do
245+ if [[ " ${CONFLICTING_GROUPS[*]} " == * " ${GROUP} " * && " $GROUP " != " $TORCH_GROUP " ]]; then
246+ echo " Error: --with-$GROUP conflicts with TORCH_GROUP=$TORCH_GROUP ." >&2
247+ echo " TORCH_GROUP is the source of truth; unset it or set it to '$GROUP ' instead." >&2
248+ exit 1
249+ fi
250+ done
251+ fi
252+
236253# Validate venv name contains only allowed characters
237254if [[ ! " $VENV " =~ ^[a-zA-Z0-9._/-]+$ ]]; then
238255 echo " Error: Virtual environment name contains invalid characters"
@@ -267,24 +284,35 @@ if [[ "$ENSURE_MODE" == "true" ]] && [ -f "$VENV/bin/python" ]; then
267284 for GROUP in " ${EXTRA_GROUPS[@]} " ; do
268285 case " $GROUP " in
269286 docs) IMPORT_STMTS+=" ; import sphinx" ;;
270- torch_2_8 | torch_2_9 | torch_2_10 | torch_2_11)
271- IMPORT_STMTS+=" ; import torchao"
272- EXPECTED_TORCH=" $( group_torch_pin " $GROUP " ) "
273- # These groups always pin torch, so an empty result means the
274- # pyproject parse regressed — fail loudly instead of silently
275- # skipping the version check (which would reintroduce the bug).
276- if [[ -z " $EXPECTED_TORCH " ]]; then
277- echo " Error: could not parse a torch pin for group '$GROUP ' in $PYPROJECT_TOML " >&2
278- exit 1
279- fi
280- IMPORT_STMTS+=" ; import torch; assert torch.__version__.split('+')[0] == '$EXPECTED_TORCH '"
281- ;;
282287 rio) IMPORT_STMTS+=" ; import turi_lightning" ;;
283288 tamm-export) IMPORT_STMTS+=" ; import tamm_export" ;;
284289 esac
285290 done
286291 fi
287292
293+ # TORCH_GROUP is the single source of truth for torch pinning; fall back
294+ # to a --with-torch_2_* flag for direct, non-Make invocations. Checked
295+ # unconditionally rather than only when present in EXTRA_GROUPS, since
296+ # TORCH_GROUP drives the sync regardless of what's in EXTRA_GROUPS.
297+ EXPECTED_GROUP=" ${TORCH_GROUP:- } "
298+ if [[ -z " $EXPECTED_GROUP " ]]; then
299+ for GROUP in " ${EXTRA_GROUPS[@]:- } " ; do
300+ [[ " ${CONFLICTING_GROUPS[*]} " == * " ${GROUP} " * ]] && EXPECTED_GROUP=" $GROUP "
301+ done
302+ fi
303+ if [[ -n " $EXPECTED_GROUP " ]]; then
304+ IMPORT_STMTS+=" ; import torchao"
305+ EXPECTED_TORCH=" $( group_torch_pin " $EXPECTED_GROUP " ) "
306+ # These groups always pin torch, so an empty result means the
307+ # pyproject parse regressed — fail loudly instead of silently
308+ # skipping the version check (which would reintroduce the bug).
309+ if [[ -z " $EXPECTED_TORCH " ]]; then
310+ echo " Error: could not parse a torch pin for '$EXPECTED_GROUP ' in $PYPROJECT_TOML " >&2
311+ exit 1
312+ fi
313+ IMPORT_STMTS+=" ; import torch; assert torch.__version__.split('+')[0] == '$EXPECTED_TORCH '"
314+ fi
315+
288316 if " $VENV /bin/python" -c " $IMPORT_STMTS " 2> /dev/null; then
289317 exit 0
290318 fi
@@ -324,9 +352,14 @@ echo "[2/3] Installing dependencies..."
324352SYNC_CMD=(uv sync --active)
325353if [[ " $ALL_GROUPS " == " true" ]]; then
326354 SYNC_CMD+=(--all-groups)
327- # Exclude conflicting groups unless explicitly requested via --with-*
355+ # Exclude conflicting torch groups other than the one TORCH_GROUP or
356+ # --with-<group> selects, so --all-groups doesn't trip the mutual
357+ # -conflicts declaration in pyproject.toml.
328358 for GROUP in " ${CONFLICTING_GROUPS[@]} " ; do
329- if [[ ! " ${EXTRA_GROUPS[*]:- } " == * " ${GROUP} " * ]]; then
359+ GROUP_REQUESTED=false
360+ [[ " $GROUP " == " ${TORCH_GROUP:- } " ]] && GROUP_REQUESTED=true
361+ [[ " ${EXTRA_GROUPS[*]:- } " == * " ${GROUP} " * ]] && GROUP_REQUESTED=true
362+ if [[ " $GROUP_REQUESTED " == " false" ]]; then
330363 SYNC_CMD+=(--no-group " $GROUP " )
331364 fi
332365 done
@@ -335,6 +368,15 @@ elif [[ ${#EXTRA_GROUPS[@]} -gt 0 ]]; then
335368 SYNC_CMD+=(--group " $GROUP " )
336369 done
337370fi
371+ # TORCH_GROUP is the single source of truth for torch pinning: fold it in
372+ # unless --all-groups already swept it up or a --with-<group> above already
373+ # requested it explicitly (the earlier guard ensures no disagreement).
374+ TORCH_GROUP_ALREADY_SYNCED=false
375+ [[ " $ALL_GROUPS " == " true" ]] && TORCH_GROUP_ALREADY_SYNCED=true
376+ [[ " ${EXTRA_GROUPS[*]:- } " == * " ${TORCH_GROUP:- } " * ]] && TORCH_GROUP_ALREADY_SYNCED=true
377+ if [[ -n " ${TORCH_GROUP:- } " && " $TORCH_GROUP_ALREADY_SYNCED " == " false" ]]; then
378+ SYNC_CMD+=(--group " $TORCH_GROUP " )
379+ fi
338380# Apply explicit group exclusions (e.g., --without-coreai)
339381if [[ ${# EXCLUDE_GROUPS[@]} -gt 0 ]]; then
340382 for GROUP in " ${EXCLUDE_GROUPS[@]} " ; do
0 commit comments