|
| 1 | +#!/bin/bash |
| 2 | +# |
| 3 | +# RingKernel Python Package Publishing Script |
| 4 | +# |
| 5 | +# This script publishes the ringkernel Python package to PyPI using maturin. |
| 6 | +# |
| 7 | +# Usage: |
| 8 | +# ./scripts/publish-python.sh <PYPI_TOKEN> |
| 9 | +# ./scripts/publish-python.sh --dry-run # Build without publishing |
| 10 | +# ./scripts/publish-python.sh --test-pypi <TOKEN> # Publish to TestPyPI first |
| 11 | +# ./scripts/publish-python.sh --status # Check if version is published |
| 12 | +# |
| 13 | +# Prerequisites: |
| 14 | +# - maturin installed: pip install maturin |
| 15 | +# - PyPI token from: https://pypi.org/manage/account/token/ |
| 16 | +# |
| 17 | +# Features to build (optional, specify with --features): |
| 18 | +# - cuda: CUDA device support |
| 19 | +# - benchmark: Benchmark framework |
| 20 | +# |
| 21 | + |
| 22 | +set -e |
| 23 | + |
| 24 | +# Colors for output |
| 25 | +RED='\033[0;31m' |
| 26 | +GREEN='\033[0;32m' |
| 27 | +YELLOW='\033[1;33m' |
| 28 | +BLUE='\033[0;34m' |
| 29 | +CYAN='\033[0;36m' |
| 30 | +NC='\033[0m' # No Color |
| 31 | + |
| 32 | +# Configuration |
| 33 | +DRY_RUN=false |
| 34 | +TEST_PYPI=false |
| 35 | +STATUS_ONLY=false |
| 36 | +FEATURES="" |
| 37 | +PACKAGE_NAME="ringkernel" |
| 38 | +CRATE_DIR="crates/ringkernel-python" |
| 39 | + |
| 40 | +# Parse arguments |
| 41 | +TOKEN="" |
| 42 | +for arg in "$@"; do |
| 43 | + case $arg in |
| 44 | + --dry-run) |
| 45 | + DRY_RUN=true |
| 46 | + shift |
| 47 | + ;; |
| 48 | + --test-pypi) |
| 49 | + TEST_PYPI=true |
| 50 | + shift |
| 51 | + ;; |
| 52 | + --status) |
| 53 | + STATUS_ONLY=true |
| 54 | + shift |
| 55 | + ;; |
| 56 | + --features=*) |
| 57 | + FEATURES="${arg#*=}" |
| 58 | + shift |
| 59 | + ;; |
| 60 | + --help|-h) |
| 61 | + echo "RingKernel Python Package Publisher" |
| 62 | + echo "" |
| 63 | + echo "Usage: $0 [OPTIONS] [TOKEN]" |
| 64 | + echo "" |
| 65 | + echo "Options:" |
| 66 | + echo " --dry-run Build wheels without publishing" |
| 67 | + echo " --test-pypi Publish to TestPyPI instead of PyPI" |
| 68 | + echo " --status Check if current version is published" |
| 69 | + echo " --features=FEAT Comma-separated features (cuda,benchmark)" |
| 70 | + echo " --help Show this help message" |
| 71 | + echo "" |
| 72 | + echo "Examples:" |
| 73 | + echo " $0 --dry-run # Test build" |
| 74 | + echo " $0 --test-pypi <TOKEN> # Publish to TestPyPI" |
| 75 | + echo " $0 <TOKEN> # Publish to PyPI" |
| 76 | + echo " $0 --features=cuda,benchmark <TOKEN>" |
| 77 | + echo "" |
| 78 | + echo "Get your PyPI token from: https://pypi.org/manage/account/token/" |
| 79 | + echo "Get TestPyPI token from: https://test.pypi.org/manage/account/token/" |
| 80 | + exit 0 |
| 81 | + ;; |
| 82 | + *) |
| 83 | + if [ -z "$TOKEN" ] && [[ ! "$arg" =~ ^-- ]]; then |
| 84 | + TOKEN="$arg" |
| 85 | + fi |
| 86 | + ;; |
| 87 | + esac |
| 88 | +done |
| 89 | + |
| 90 | +print_header() { |
| 91 | + echo "" |
| 92 | + echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" |
| 93 | + echo -e "${BLUE} $1${NC}" |
| 94 | + echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" |
| 95 | + echo "" |
| 96 | +} |
| 97 | + |
| 98 | +print_step() { |
| 99 | + echo -e "${GREEN}▶${NC} $1" |
| 100 | +} |
| 101 | + |
| 102 | +print_warning() { |
| 103 | + echo -e "${YELLOW}⚠${NC} $1" |
| 104 | +} |
| 105 | + |
| 106 | +print_error() { |
| 107 | + echo -e "${RED}✖${NC} $1" |
| 108 | +} |
| 109 | + |
| 110 | +print_success() { |
| 111 | + echo -e "${GREEN}✔${NC} $1" |
| 112 | +} |
| 113 | + |
| 114 | +get_version() { |
| 115 | + # Get version from pyproject.toml |
| 116 | + grep '^version = ' "$CRATE_DIR/pyproject.toml" | sed 's/version = "\(.*\)"/\1/' | head -1 |
| 117 | +} |
| 118 | + |
| 119 | +check_pypi_published() { |
| 120 | + local version=$1 |
| 121 | + local registry_url="https://pypi.org/pypi/$PACKAGE_NAME/$version/json" |
| 122 | + |
| 123 | + if [ "$TEST_PYPI" = true ]; then |
| 124 | + registry_url="https://test.pypi.org/pypi/$PACKAGE_NAME/$version/json" |
| 125 | + fi |
| 126 | + |
| 127 | + local response=$(curl -s -o /dev/null -w "%{http_code}" "$registry_url" 2>/dev/null) |
| 128 | + if [ "$response" = "200" ]; then |
| 129 | + return 0 |
| 130 | + fi |
| 131 | + return 1 |
| 132 | +} |
| 133 | + |
| 134 | +check_maturin() { |
| 135 | + if ! command -v maturin &> /dev/null; then |
| 136 | + print_error "maturin is not installed!" |
| 137 | + echo "" |
| 138 | + echo "Install it with:" |
| 139 | + echo " pip install maturin" |
| 140 | + echo "" |
| 141 | + echo "Or with pipx:" |
| 142 | + echo " pipx install maturin" |
| 143 | + exit 1 |
| 144 | + fi |
| 145 | + print_success "maturin $(maturin --version | cut -d' ' -f2) found" |
| 146 | +} |
| 147 | + |
| 148 | +# Main script |
| 149 | +print_header "RingKernel Python Package Publisher" |
| 150 | + |
| 151 | +# Change to workspace root |
| 152 | +cd "$(dirname "$0")/.." |
| 153 | + |
| 154 | +# Check prerequisites |
| 155 | +print_step "Checking prerequisites..." |
| 156 | +check_maturin |
| 157 | + |
| 158 | +# Verify crate directory exists |
| 159 | +if [ ! -d "$CRATE_DIR" ]; then |
| 160 | + print_error "Crate directory not found: $CRATE_DIR" |
| 161 | + exit 1 |
| 162 | +fi |
| 163 | +print_success "Found $CRATE_DIR" |
| 164 | + |
| 165 | +VERSION=$(get_version) |
| 166 | +if [ -z "$VERSION" ]; then |
| 167 | + print_error "Could not determine version from pyproject.toml" |
| 168 | + exit 1 |
| 169 | +fi |
| 170 | + |
| 171 | +# Determine registry |
| 172 | +REGISTRY="PyPI" |
| 173 | +REGISTRY_URL="https://pypi.org/project/$PACKAGE_NAME/" |
| 174 | +if [ "$TEST_PYPI" = true ]; then |
| 175 | + REGISTRY="TestPyPI" |
| 176 | + REGISTRY_URL="https://test.pypi.org/project/$PACKAGE_NAME/" |
| 177 | +fi |
| 178 | + |
| 179 | +echo "" |
| 180 | +echo "Configuration:" |
| 181 | +echo " Package: $PACKAGE_NAME" |
| 182 | +echo " Version: $VERSION" |
| 183 | +echo " Registry: $REGISTRY" |
| 184 | +echo " Features: ${FEATURES:-none}" |
| 185 | +echo " Dry run: $DRY_RUN" |
| 186 | +echo " Crate dir: $CRATE_DIR" |
| 187 | + |
| 188 | +# Status check |
| 189 | +if [ "$STATUS_ONLY" = true ]; then |
| 190 | + print_header "Publishing Status" |
| 191 | + echo -n " $PACKAGE_NAME@$VERSION on $REGISTRY: " |
| 192 | + if check_pypi_published "$VERSION"; then |
| 193 | + echo -e "${GREEN}✔ published${NC}" |
| 194 | + echo "" |
| 195 | + echo "View at: $REGISTRY_URL$VERSION/" |
| 196 | + else |
| 197 | + echo -e "${YELLOW}○ not published${NC}" |
| 198 | + fi |
| 199 | + exit 0 |
| 200 | +fi |
| 201 | + |
| 202 | +# Check if already published |
| 203 | +print_header "Checking Publishing Status" |
| 204 | +echo -n " $PACKAGE_NAME@$VERSION on $REGISTRY: " |
| 205 | +if check_pypi_published "$VERSION"; then |
| 206 | + echo -e "${GREEN}✔ already published${NC}" |
| 207 | + echo "" |
| 208 | + print_success "Version $VERSION is already published on $REGISTRY" |
| 209 | + echo "" |
| 210 | + echo "View at: $REGISTRY_URL$VERSION/" |
| 211 | + exit 0 |
| 212 | +else |
| 213 | + echo -e "${YELLOW}○ not published${NC}" |
| 214 | +fi |
| 215 | + |
| 216 | +# Validate token for real publish |
| 217 | +if [ "$DRY_RUN" = false ] && [ -z "$TOKEN" ]; then |
| 218 | + echo "" |
| 219 | + print_error "No PyPI token provided!" |
| 220 | + echo "" |
| 221 | + echo "Usage: $0 <PYPI_TOKEN> [--test-pypi]" |
| 222 | + echo "" |
| 223 | + echo "Get your token from:" |
| 224 | + echo " PyPI: https://pypi.org/manage/account/token/" |
| 225 | + echo " TestPyPI: https://test.pypi.org/manage/account/token/" |
| 226 | + exit 1 |
| 227 | +fi |
| 228 | + |
| 229 | +# Build the package |
| 230 | +print_header "Building Package" |
| 231 | + |
| 232 | +cd "$CRATE_DIR" |
| 233 | + |
| 234 | +BUILD_ARGS="--release" |
| 235 | +if [ -n "$FEATURES" ]; then |
| 236 | + BUILD_ARGS="$BUILD_ARGS --features $FEATURES" |
| 237 | +fi |
| 238 | + |
| 239 | +print_step "Running: maturin build $BUILD_ARGS" |
| 240 | +echo "" |
| 241 | + |
| 242 | +if maturin build $BUILD_ARGS; then |
| 243 | + print_success "Build completed successfully" |
| 244 | +else |
| 245 | + print_error "Build failed" |
| 246 | + exit 1 |
| 247 | +fi |
| 248 | + |
| 249 | +# Show built wheels |
| 250 | +echo "" |
| 251 | +print_step "Built wheels:" |
| 252 | +ls -la target/wheels/*.whl 2>/dev/null | sed 's/^/ /' |
| 253 | + |
| 254 | +if [ "$DRY_RUN" = true ]; then |
| 255 | + print_header "Dry Run Complete" |
| 256 | + print_success "Package built successfully (not published)" |
| 257 | + echo "" |
| 258 | + echo "Wheels are in: $CRATE_DIR/target/wheels/" |
| 259 | + echo "" |
| 260 | + echo "To publish for real, run:" |
| 261 | + echo " $0 <PYPI_TOKEN>" |
| 262 | + exit 0 |
| 263 | +fi |
| 264 | + |
| 265 | +# Publish |
| 266 | +print_header "Publishing to $REGISTRY" |
| 267 | + |
| 268 | +PUBLISH_ARGS="--username __token__ --password $TOKEN" |
| 269 | +if [ "$TEST_PYPI" = true ]; then |
| 270 | + PUBLISH_ARGS="$PUBLISH_ARGS --repository-url https://test.pypi.org/legacy/" |
| 271 | +fi |
| 272 | +if [ -n "$FEATURES" ]; then |
| 273 | + PUBLISH_ARGS="$PUBLISH_ARGS --features $FEATURES" |
| 274 | +fi |
| 275 | + |
| 276 | +print_step "Publishing $PACKAGE_NAME@$VERSION to $REGISTRY..." |
| 277 | +echo "" |
| 278 | + |
| 279 | +# Hide token in output |
| 280 | +if maturin publish $PUBLISH_ARGS 2>&1 | grep -v "$TOKEN"; then |
| 281 | + echo "" |
| 282 | + print_success "Successfully published $PACKAGE_NAME@$VERSION to $REGISTRY!" |
| 283 | +else |
| 284 | + echo "" |
| 285 | + print_error "Failed to publish" |
| 286 | + exit 1 |
| 287 | +fi |
| 288 | + |
| 289 | +# Verify publication |
| 290 | +print_header "Verifying Publication" |
| 291 | +echo "Waiting for package to appear on $REGISTRY..." |
| 292 | +sleep 5 |
| 293 | + |
| 294 | +MAX_WAIT=60 |
| 295 | +WAITED=0 |
| 296 | +while [ $WAITED -lt $MAX_WAIT ]; do |
| 297 | + if check_pypi_published "$VERSION"; then |
| 298 | + print_success "Package is now available on $REGISTRY" |
| 299 | + break |
| 300 | + fi |
| 301 | + echo -ne "\r Waiting... [$WAITED/$MAX_WAIT seconds]" |
| 302 | + sleep 5 |
| 303 | + WAITED=$((WAITED + 5)) |
| 304 | +done |
| 305 | + |
| 306 | +echo "" |
| 307 | + |
| 308 | +# Summary |
| 309 | +print_header "Summary" |
| 310 | +print_success "Published: $PACKAGE_NAME@$VERSION" |
| 311 | +echo "" |
| 312 | +echo "Install with:" |
| 313 | +if [ "$TEST_PYPI" = true ]; then |
| 314 | + echo " pip install --index-url https://test.pypi.org/simple/ $PACKAGE_NAME" |
| 315 | +else |
| 316 | + echo " pip install $PACKAGE_NAME" |
| 317 | +fi |
| 318 | +echo "" |
| 319 | +echo "View at: $REGISTRY_URL" |
0 commit comments