Skip to content

Commit 01c24b8

Browse files
authored
RHAIENG-1649: feat: add automated dependency lock generation script (#2644)
* feat: add automated dependency lock generation script (pylock_generator.sh) * Update the script logic to utilize index_mode this give us the freedom to use whatever we need according our needs * Update the makefile recipe where updates the locks files accordingly
1 parent 27726a8 commit 01c24b8

File tree

2 files changed

+265
-65
lines changed

2 files changed

+265
-65
lines changed

Makefile

Lines changed: 17 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ RELEASE_PYTHON_VERSION ?= 3.12
2525
CONTAINER_BUILD_CACHE_ARGS ?= --no-cache
2626
# whether to push the images to a registry as they are built
2727
PUSH_IMAGES ?= yes
28+
# INDEX_MODES=public-index(default) or aipcc-index this used for the lock
29+
INDEX_MODE ?= public-index
2830

2931
# OS dependant: Generate date, select appropriate cmd to locate container engine
3032
ifdef OS
@@ -113,7 +115,7 @@ endef
113115
####################################### Build helpers #######################################
114116

115117
# https://stackoverflow.com/questions/78899903/how-to-create-a-make-target-which-is-an-implicit-dependency-for-all-other-target
116-
skip-init-for := all-images deploy% undeploy% test% validate% refresh-pipfilelock-files scan-image-vulnerabilities print-release
118+
skip-init-for := all-images deploy% undeploy% test% validate% refresh-lock-files scan-image-vulnerabilities print-release
117119
ifneq (,$(filter-out $(skip-init-for),$(MAKECMDGOALS) $(.DEFAULT_GOAL)))
118120
$(SELF): bin/buildinputs
119121
endif
@@ -406,70 +408,20 @@ validate-rstudio-image: bin/kubectl
406408
continue
407409
fi
408410

409-
# This recipe used mainly from the Pipfile.locks Renewal Action
410-
# Default Python version
411-
PYTHON_VERSION ?= 3.12
412-
ROOT_DIR := $(shell pwd)
413-
ifeq ($(PYTHON_VERSION), 3.12)
414-
BASE_DIRS := \
415-
jupyter/minimal/ubi9-python-$(PYTHON_VERSION) \
416-
jupyter/datascience/ubi9-python-$(PYTHON_VERSION) \
417-
jupyter/pytorch/ubi9-python-$(PYTHON_VERSION) \
418-
jupyter/tensorflow/ubi9-python-$(PYTHON_VERSION) \
419-
jupyter/trustyai/ubi9-python-$(PYTHON_VERSION) \
420-
jupyter/rocm/pytorch/ubi9-python-$(PYTHON_VERSION) \
421-
jupyter/pytorch+llmcompressor/ubi9-python-$(PYTHON_VERSION) \
422-
codeserver/ubi9-python-$(PYTHON_VERSION) \
423-
runtimes/minimal/ubi9-python-$(PYTHON_VERSION) \
424-
runtimes/datascience/ubi9-python-$(PYTHON_VERSION) \
425-
runtimes/pytorch/ubi9-python-$(PYTHON_VERSION) \
426-
runtimes/tensorflow/ubi9-python-$(PYTHON_VERSION) \
427-
runtimes/rocm-pytorch/ubi9-python-$(PYTHON_VERSION) \
428-
runtimes/pytorch+llmcompressor/ubi9-python-$(PYTHON_VERSION) \
429-
runtimes/rocm-tensorflow/ubi9-python-$(PYTHON_VERSION) \
430-
jupyter/rocm/tensorflow/ubi9-python-$(PYTHON_VERSION) \
431-
rstudio/rhel9-python-$(PYTHON_VERSION) \
432-
rstudio/c9s-python-$(PYTHON_VERSION)
433-
else
434-
$(error Invalid Python version $(PYTHON_VERSION))
435-
endif
436-
437-
# Default value is false, can be overridden
438-
# The below directories are not supported on tier-1
439-
INCLUDE_OPT_DIRS ?= false
440-
OPT_DIRS :=
441-
442-
# This recipe gets args, can be used like
443-
# make refresh-pipfilelock-files PYTHON_VERSION=3.11 INCLUDE_OPT_DIRS=false
444-
.PHONY: refresh-pipfilelock-files
445-
refresh-pipfilelock-files:
446-
@echo "Updating Pipfile.lock files for Python $(PYTHON_VERSION)"
447-
@if [ "$(INCLUDE_OPT_DIRS)" = "true" ]; then
448-
echo "Including optional directories"
449-
DIRS="$(BASE_DIRS) $(OPT_DIRS)"
450-
else
451-
DIRS="$(BASE_DIRS)"
452-
fi
453-
for dir in $$DIRS; do
454-
echo "Processing directory: $$dir"
455-
cd $(ROOT_DIR)
456-
if [ -d "$$dir" ]; then
457-
echo "Updating $(PYTHON_VERSION) uv.lock in $$dir"
458-
cd $$dir
459-
if [ -f "pyproject.toml" ]; then
460-
uv lock && rm uv.lock
461-
else
462-
echo "No pyproject.toml found in $$dir, skipping."
463-
fi
464-
else
465-
echo "Skipping $$dir as it does not exist"
466-
fi
467-
done
468-
469-
echo "Regenerating requirements.txt files"
470-
pushd $(ROOT_DIR)
471-
bash $(ROOT_DIR)/scripts/sync-python-lockfiles.sh
472-
popd
411+
# ======================================================================================
412+
# Refresh lock files
413+
# Usage examples:
414+
# gmake refresh-lock-files
415+
# gmake refresh-lock-files INDEX_MODE=aipcc-index
416+
# gmake refresh-lock-files INDEX_MODE=aipcc-index DIR=jupyter/minimal/ubi9-python-3.12
417+
# ======================================================================================
418+
DIR ?=
419+
.PHONY: refresh-lock-files
420+
refresh-lock-files:
421+
@echo "==================================================================="
422+
@echo "🔁 Refreshing pylock.toml files using $(INDEX_MODE)"
423+
@echo "==================================================================="
424+
@cd $(ROOT_DIR) && bash scripts/pylocks_generator.sh $(INDEX_MODE) $(DIR)
473425

474426
# This is only for the workflow action
475427
# For running manually, set the required environment variables

scripts/pylocks_generator.sh

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# =============================================================================
5+
# pylocks_generator.sh
6+
#
7+
# This script generates Python dependency lock files (pylock.toml) for multiple
8+
# directories using either internal AIPCC wheel indexes or the public PyPI index.
9+
#
10+
# Features:
11+
# • Supports multiple Python project directories, detected by pyproject.toml.
12+
# • Detects available Dockerfile flavors (CPU, CUDA, ROCm) for AIPCC index mode.
13+
# • Validates Python version extracted from directory name (expects format .../ubi9-python-X.Y).
14+
# • Generates per-flavor locks in 'uv.lock/' for AIPCC index mode.
15+
# • Overwrites existing pylock.toml in-place for public PyPI index mode.
16+
#
17+
# Index Modes:
18+
# • aipcc-index -> Uses internal Red Hat AIPCC wheel indexes. Generates uv.lock/pylock.<flavor>.toml for each detected flavor.
19+
# • public-index -> Uses public PyPI index. Updates pylock.toml in the project directory.
20+
# Default mode if not specified.
21+
#
22+
# Usage:
23+
# 1. Lock using default public index for all projects in MAIN_DIRS:
24+
# bash pylocks_generator.sh
25+
#
26+
# 2. Lock using AIPCC index for a specific directory:
27+
# bash pylocks_generator.sh aipcc-index jupyter/minimal/ubi9-python-3.12
28+
#
29+
# 3. Lock using public index for a specific directory:
30+
# bash pylocks_generator.sh public-index jupyter/minimal/ubi9-python-3.12
31+
#
32+
# Notes:
33+
# • If the script fails for a directory, it lists the failed directories at the end.
34+
# • Public index mode does not create uv.lock directories keeps the old format.
35+
# • Python version extraction depends on directory naming convention; invalid formats are skipped.
36+
# =============================================================================
37+
38+
# ----------------------------
39+
# CONFIGURATION
40+
# ----------------------------
41+
CPU_INDEX="--index-url=https://console.redhat.com/api/pypi/public-rhai/rhoai/3.0/cpu-ubi9/simple/"
42+
CUDA_INDEX="--index-url=https://console.redhat.com/api/pypi/public-rhai/rhoai/3.0/cuda-ubi9/simple/"
43+
ROCM_INDEX="--index-url=https://console.redhat.com/api/pypi/public-rhai/rhoai/3.0/rocm-ubi9/simple/"
44+
PUBLIC_INDEX="--index-url=https://pypi.org/simple"
45+
46+
MAIN_DIRS=("jupyter" "runtimes" "rstudio" "codeserver")
47+
48+
# ----------------------------
49+
# HELPER FUNCTIONS
50+
# ----------------------------
51+
info() { echo -e "🔹 \033[1;34m$1\033[0m"; }
52+
warn() { echo -e "⚠️ \033[1;33m$1\033[0m"; }
53+
error() { echo -e "❌ \033[1;31m$1\033[0m"; }
54+
ok() { echo -e "✅ \033[1;32m$1\033[0m"; }
55+
56+
uppercase() {
57+
echo "$1" | tr '[:lower:]' '[:upper:]'
58+
}
59+
60+
# ----------------------------
61+
# PRE-FLIGHT CHECK
62+
# ----------------------------
63+
if ! command -v uv &>/dev/null; then
64+
error "uv command not found. Please install uv: https://github.com/astral-sh/uv"
65+
exit 1
66+
fi
67+
68+
UV_MIN_VERSION="0.4.0"
69+
UV_VERSION=$(uv --version 2>/dev/null | awk '{print $2}' || echo "0.0.0")
70+
71+
version_ge() {
72+
[ "$(printf '%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ]
73+
}
74+
75+
if ! version_ge "$UV_VERSION" "$UV_MIN_VERSION"; then
76+
error "uv version $UV_VERSION found, but >= $UV_MIN_VERSION is required."
77+
error "Please upgrade uv: https://github.com/astral-sh/uv"
78+
exit 1
79+
fi
80+
81+
# ----------------------------
82+
# ARGUMENT PARSING
83+
# ----------------------------
84+
# default to public-index if not provided
85+
INDEX_MODE="${1:-public-index}"
86+
TARGET_DIR_ARG="${2:-}"
87+
88+
# Validate mode
89+
if [[ "$INDEX_MODE" != "aipcc-index" && "$INDEX_MODE" != "public-index" ]]; then
90+
error "Invalid mode '$INDEX_MODE'. Valid options: aipcc-index, public-index"
91+
exit 1
92+
fi
93+
info "Using index mode: $INDEX_MODE"
94+
95+
# ----------------------------
96+
# GET TARGET DIRECTORIES
97+
# ----------------------------
98+
if [ -n "$TARGET_DIR_ARG" ]; then
99+
TARGET_DIRS=("$TARGET_DIR_ARG")
100+
else
101+
info "Scanning main directories for Python projects..."
102+
TARGET_DIRS=()
103+
for base in "${MAIN_DIRS[@]}"; do
104+
if [ -d "$base" ]; then
105+
while IFS= read -r -d '' pyproj; do
106+
TARGET_DIRS+=("$(dirname "$pyproj")")
107+
done < <(find "$base" -type f -name "pyproject.toml" -print0)
108+
fi
109+
done
110+
fi
111+
112+
if [ ${#TARGET_DIRS[@]} -eq 0 ]; then
113+
error "No directories containing pyproject.toml were found."
114+
exit 1
115+
fi
116+
117+
# ----------------------------
118+
# MAIN LOOP
119+
# ----------------------------
120+
FAILED_DIRS=()
121+
SUCCESS_DIRS=()
122+
123+
for TARGET_DIR in "${TARGET_DIRS[@]}"; do
124+
echo
125+
echo "==================================================================="
126+
info "Processing directory: $TARGET_DIR"
127+
echo "==================================================================="
128+
129+
cd "$TARGET_DIR" || continue
130+
PYTHON_VERSION="${PWD##*-}"
131+
132+
# Validate Python version extraction
133+
if [[ ! "$PYTHON_VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then
134+
warn "Could not extract valid Python version from directory name: $PWD"
135+
warn "Expected directory format: .../ubi9-python-X.Y"
136+
cd - >/dev/null
137+
continue
138+
fi
139+
140+
# Detect available Dockerfiles (flavors)
141+
HAS_CPU=false
142+
HAS_CUDA=false
143+
HAS_ROCM=false
144+
[ -f "Dockerfile.cpu" ] && HAS_CPU=true
145+
[ -f "Dockerfile.cuda" ] && HAS_CUDA=true
146+
[ -f "Dockerfile.rocm" ] && HAS_ROCM=true
147+
148+
if ! $HAS_CPU && ! $HAS_CUDA && ! $HAS_ROCM; then
149+
warn "No Dockerfiles found in $TARGET_DIR (cpu/cuda/rocm). Skipping."
150+
cd - >/dev/null
151+
continue
152+
fi
153+
154+
echo "📦 Python version: $PYTHON_VERSION"
155+
echo "🧩 Detected flavors:"
156+
$HAS_CPU && echo " • CPU"
157+
$HAS_CUDA && echo " • CUDA"
158+
$HAS_ROCM && echo " • ROCm"
159+
echo
160+
161+
DIR_SUCCESS=true
162+
163+
run_lock() {
164+
local flavor="$1"
165+
local index="$2"
166+
local output
167+
local desc
168+
169+
if [[ "$INDEX_MODE" == "public-index" ]]; then
170+
output="pylock.toml"
171+
desc="pylock.toml (public index)"
172+
echo "➡️ Generating pylock.toml from public PyPI index..."
173+
else
174+
mkdir -p uv.lock
175+
output="uv.lock/pylock.${flavor}.toml"
176+
desc="$(uppercase "$flavor") lock file"
177+
echo "➡️ Generating $(uppercase "$flavor") lock file..."
178+
fi
179+
180+
set +e
181+
uv pip compile pyproject.toml \
182+
--output-file "$output" \
183+
--format pylock.toml \
184+
--generate-hashes \
185+
--emit-index-url \
186+
--python-version="$PYTHON_VERSION" \
187+
--python-platform linux \
188+
--no-annotate \
189+
$index
190+
local status=$?
191+
set -e
192+
193+
if [ $status -ne 0 ]; then
194+
warn "Failed to generate $desc in $TARGET_DIR"
195+
rm -f "$output"
196+
DIR_SUCCESS=false
197+
else
198+
if [[ "$INDEX_MODE" == "public-index" ]]; then
199+
ok "pylock.toml generated successfully."
200+
else
201+
ok "$(uppercase "$flavor") lock generated successfully."
202+
fi
203+
fi
204+
}
205+
206+
# Run lock generation
207+
if [[ "$INDEX_MODE" == "public-index" ]]; then
208+
# public-index always updates pylock.toml in place
209+
run_lock "cpu" "$PUBLIC_INDEX"
210+
else
211+
$HAS_CPU && run_lock "cpu" "$CPU_INDEX"
212+
$HAS_CUDA && run_lock "cuda" "$CUDA_INDEX"
213+
$HAS_ROCM && run_lock "rocm" "$ROCM_INDEX"
214+
fi
215+
216+
if $DIR_SUCCESS; then
217+
SUCCESS_DIRS+=("$TARGET_DIR")
218+
else
219+
FAILED_DIRS+=("$TARGET_DIR")
220+
fi
221+
222+
cd - >/dev/null
223+
done
224+
225+
# ----------------------------
226+
# SUMMARY
227+
# ----------------------------
228+
echo
229+
echo "==================================================================="
230+
ok "Lock generation complete."
231+
echo "==================================================================="
232+
233+
if [ ${#SUCCESS_DIRS[@]} -gt 0 ]; then
234+
echo "✅ Successfully generated locks for:"
235+
for d in "${SUCCESS_DIRS[@]}"; do
236+
echo "$d"
237+
done
238+
fi
239+
240+
if [ ${#FAILED_DIRS[@]} -gt 0 ]; then
241+
echo
242+
warn "Failed lock generation for:"
243+
for d in "${FAILED_DIRS[@]}"; do
244+
echo "$d"
245+
echo "Please comment out the missing package to continue and report the missing package to aipcc"
246+
done
247+
exit 1
248+
fi

0 commit comments

Comments
 (0)