@osfactory/har 0.61.2 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +102 -40
- package/dist/templates/har-boilerplate/agent-slot.sh +23 -0
- package/dist/templates/har-boilerplate/provision-toolchain.sh +258 -19
- package/dist/templates/har-boilerplate/verify.sh +6 -19
- package/dist/templates/har-boilerplate-cli/agent-slot.sh +23 -0
- package/dist/templates/har-boilerplate-cli/provision-toolchain.sh +258 -19
- package/dist/templates/har-boilerplate-cli/verify.sh +4 -10
- package/dist/templates/har-boilerplate-ios/agent-slot.sh +23 -0
- package/dist/templates/har-boilerplate-ios/provision-toolchain.sh +258 -19
- package/dist/templates/har-boilerplate-ios/verify.sh +4 -10
- package/dist/templates/runtime-bundles/shared-kernel/provision-toolchain.sh +258 -19
- package/package.json +1 -1
|
@@ -228,52 +228,291 @@ provision_node() {
|
|
|
228
228
|
append_path_prefix "$dir/node_modules/.bin"
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
# ── Python interpreter helpers ────────────────────────────────────────────────
|
|
232
|
+
# Resolve the project Python (requires-python, .python-version, uv) instead of
|
|
233
|
+
# blindly using PATH python3.
|
|
234
|
+
|
|
235
|
+
har_python_version_from_bin() {
|
|
236
|
+
local bin="$1"
|
|
237
|
+
"$bin" -c 'import sys; print(".".join(map(str, sys.version_info[:3])))' 2>/dev/null \
|
|
238
|
+
| tr -d '[:space:]'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
har_python_version_to_sortable() {
|
|
242
|
+
local v="${1#python}"
|
|
243
|
+
v="${v#v}"
|
|
244
|
+
local major=0 minor=0 patch=0
|
|
245
|
+
IFS='.' read -r major minor patch _ <<< "$v"
|
|
246
|
+
major=${major:-0}
|
|
247
|
+
minor=${minor:-0}
|
|
248
|
+
patch=${patch:-0}
|
|
249
|
+
printf '%03d%03d%03d' "$major" "$minor" "$patch"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
har_python_version_ge() {
|
|
253
|
+
local a b
|
|
254
|
+
a="$(har_python_version_to_sortable "$1")"
|
|
255
|
+
b="$(har_python_version_to_sortable "$2")"
|
|
256
|
+
[ "$a" -ge "$b" ]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
har_python_requires_minimum() {
|
|
260
|
+
local spec="$1"
|
|
261
|
+
local ver=""
|
|
262
|
+
|
|
263
|
+
[ -n "$spec" ] || return 1
|
|
264
|
+
|
|
265
|
+
ver="$(printf '%s' "$spec" | sed -n \
|
|
266
|
+
-e 's/.*>=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
|
|
267
|
+
-e 's/.*~=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
|
|
268
|
+
-e 's/.*==\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
|
|
269
|
+
| head -1)"
|
|
270
|
+
|
|
271
|
+
if [ -z "$ver" ]; then
|
|
272
|
+
ver="$(printf '%s' "$spec" | sed -n 's/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p')"
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
[ -n "$ver" ] && echo "$ver"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
har_python_read_dot_version() {
|
|
279
|
+
local dir="$1"
|
|
280
|
+
[ -f "$dir/.python-version" ] || return 1
|
|
281
|
+
head -1 "$dir/.python-version" | tr -d '[:space:]'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
har_python_read_requires_spec() {
|
|
285
|
+
local dir="$1"
|
|
286
|
+
local spec=""
|
|
287
|
+
|
|
288
|
+
if [ -f "$dir/pyproject.toml" ]; then
|
|
289
|
+
spec="$(sed -n 's/^[[:space:]]*requires-python[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
|
|
290
|
+
"$dir/pyproject.toml" | head -1)"
|
|
291
|
+
fi
|
|
292
|
+
if [ -z "$spec" ] && [ -f "$dir/setup.cfg" ]; then
|
|
293
|
+
spec="$(sed -n '/^\[options\]/,/^\[/ s/^python_requires[[:space:]]*=[[:space:]]*\(.*\)/\1/p' \
|
|
294
|
+
"$dir/setup.cfg" | head -1 | tr -d ' "')"
|
|
295
|
+
fi
|
|
296
|
+
[ -n "$spec" ] && echo "$spec"
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
har_python_project_minimum() {
|
|
300
|
+
local dir="$1"
|
|
301
|
+
local req_min="" dot_ver="" higher=""
|
|
302
|
+
|
|
303
|
+
req_min="$(har_python_requires_minimum "$(har_python_read_requires_spec "$dir" || true)" || true)"
|
|
304
|
+
dot_ver="$(har_python_read_dot_version "$dir" || true)"
|
|
305
|
+
if [ -n "$dot_ver" ]; then
|
|
306
|
+
dot_ver="$(printf '%s' "$dot_ver" | sed 's/^v//; s/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/')"
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
if [ -n "$req_min" ] && [ -n "$dot_ver" ]; then
|
|
310
|
+
if har_python_version_ge "$dot_ver" "$req_min"; then
|
|
311
|
+
higher="$dot_ver"
|
|
312
|
+
else
|
|
313
|
+
higher="$req_min"
|
|
314
|
+
fi
|
|
315
|
+
elif [ -n "$req_min" ]; then
|
|
316
|
+
higher="$req_min"
|
|
317
|
+
elif [ -n "$dot_ver" ]; then
|
|
318
|
+
higher="$dot_ver"
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
[ -n "$higher" ] && echo "$higher"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
har_python_is_uv_project() {
|
|
325
|
+
local dir="$1"
|
|
326
|
+
[ -f "$dir/uv.lock" ] && return 0
|
|
327
|
+
[ -f "$dir/pyproject.toml" ] && grep -q '^\[tool\.uv\]' "$dir/pyproject.toml" 2>/dev/null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
har_python_uv_find() {
|
|
331
|
+
local dir="$1"
|
|
332
|
+
local request="${2:-}"
|
|
333
|
+
|
|
334
|
+
command -v uv >/dev/null 2>&1 || return 1
|
|
335
|
+
if [ -n "$request" ]; then
|
|
336
|
+
(cd "$dir" && uv python find "$request" 2>/dev/null)
|
|
337
|
+
else
|
|
338
|
+
(cd "$dir" && uv python find 2>/dev/null)
|
|
339
|
+
fi
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
har_python_resolve_interpreter() {
|
|
343
|
+
local dir="$1"
|
|
344
|
+
local minimum="${2:-}"
|
|
345
|
+
local found=""
|
|
346
|
+
|
|
347
|
+
if har_python_is_uv_project "$dir"; then
|
|
348
|
+
found="$(har_python_uv_find "$dir" "$minimum" || true)"
|
|
349
|
+
if [ -n "$found" ] && [ -x "$found" ]; then
|
|
350
|
+
echo "$found"
|
|
351
|
+
return 0
|
|
352
|
+
fi
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
if [ -f "$dir/.python-version" ]; then
|
|
356
|
+
found="$(har_python_uv_find "$dir" "$(har_python_read_dot_version "$dir")" || true)"
|
|
357
|
+
if [ -n "$found" ] && [ -x "$found" ]; then
|
|
358
|
+
echo "$found"
|
|
359
|
+
return 0
|
|
360
|
+
fi
|
|
361
|
+
if command -v pyenv >/dev/null 2>&1; then
|
|
362
|
+
found="$(cd "$dir" && pyenv which python 2>/dev/null || true)"
|
|
363
|
+
if [ -n "$found" ] && [ -x "$found" ]; then
|
|
364
|
+
echo "$found"
|
|
365
|
+
return 0
|
|
366
|
+
fi
|
|
367
|
+
fi
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
if [ -n "$minimum" ]; then
|
|
371
|
+
found="$(har_python_uv_find "$dir" "$minimum" || true)"
|
|
372
|
+
if [ -n "$found" ] && [ -x "$found" ]; then
|
|
373
|
+
echo "$found"
|
|
374
|
+
return 0
|
|
375
|
+
fi
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
379
|
+
local sys_py sys_ver=""
|
|
380
|
+
sys_py="$(command -v python3)"
|
|
381
|
+
if [ -z "$minimum" ]; then
|
|
382
|
+
echo "$sys_py"
|
|
383
|
+
return 0
|
|
384
|
+
fi
|
|
385
|
+
sys_ver="$(har_python_version_from_bin "$sys_py")"
|
|
386
|
+
if [ -n "$sys_ver" ] && har_python_version_ge "$sys_ver" "$minimum"; then
|
|
387
|
+
echo "$sys_py"
|
|
388
|
+
return 0
|
|
389
|
+
fi
|
|
390
|
+
echo "$sys_py"
|
|
391
|
+
return 0
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
return 1
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
har_python_warn_if_below_minimum() {
|
|
398
|
+
local bin="$1"
|
|
399
|
+
local minimum="$2"
|
|
400
|
+
local actual=""
|
|
401
|
+
|
|
402
|
+
[ -n "$minimum" ] || return 0
|
|
403
|
+
actual="$(har_python_version_from_bin "$bin")"
|
|
404
|
+
[ -n "$actual" ] || return 0
|
|
405
|
+
if ! har_python_version_ge "$actual" "$minimum"; then
|
|
406
|
+
pt_log "WARNING: resolved Python ${actual} is older than required ${minimum} (requires-python / .python-version)"
|
|
407
|
+
fi
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
har_python_create_venv() {
|
|
411
|
+
local dir="$1"
|
|
412
|
+
local venv_dir="$2"
|
|
413
|
+
local interpreter="$3"
|
|
414
|
+
local venv_rel="${venv_dir#"$dir"/}"
|
|
415
|
+
|
|
416
|
+
if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
|
|
417
|
+
pt_log "Creating Python venv with uv at ${venv_rel}..."
|
|
418
|
+
if (cd "$dir" && uv venv --python "$interpreter" "$venv_dir" 2>/dev/null) \
|
|
419
|
+
|| (cd "$dir" && uv venv "$venv_dir" 2>/dev/null); then
|
|
420
|
+
return 0
|
|
421
|
+
fi
|
|
422
|
+
pt_log "uv venv failed — falling back to ${interpreter} -m venv"
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
pt_log "Creating Python venv at ${venv_rel}..."
|
|
426
|
+
"$interpreter" -m venv "$venv_dir"
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
har_python_install_deps() {
|
|
430
|
+
local dir="$1"
|
|
431
|
+
local venv_dir="$2"
|
|
432
|
+
|
|
433
|
+
if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
|
|
434
|
+
pt_log "Syncing Python dependencies with uv..."
|
|
435
|
+
if (cd "$dir" && UV_PROJECT_ENVIRONMENT="$venv_dir" uv sync --quiet 2>/dev/null); then
|
|
436
|
+
return 0
|
|
437
|
+
fi
|
|
438
|
+
if (cd "$dir" && uv sync --quiet 2>/dev/null); then
|
|
439
|
+
return 0
|
|
440
|
+
fi
|
|
441
|
+
pt_log "uv sync failed — falling back to pip"
|
|
442
|
+
return 1
|
|
443
|
+
fi
|
|
444
|
+
return 1
|
|
445
|
+
}
|
|
446
|
+
|
|
231
447
|
provision_python() {
|
|
232
448
|
local dir="$1"
|
|
233
449
|
local venv_rel="${HARNESS_PYTHON_VENV_DIR:-.har/venv}"
|
|
234
450
|
local venv_dir="$dir/$venv_rel"
|
|
235
|
-
local python_bin="
|
|
451
|
+
local python_bin=""
|
|
452
|
+
local interpreter=""
|
|
453
|
+
local project_min=""
|
|
454
|
+
|
|
455
|
+
project_min="$(har_python_project_minimum "$dir" || true)"
|
|
456
|
+
interpreter="$(har_python_resolve_interpreter "$dir" "$project_min" || true)"
|
|
236
457
|
|
|
237
|
-
if ! command -v
|
|
238
|
-
pt_log "
|
|
458
|
+
if [ -z "$interpreter" ] || ! command -v "$interpreter" >/dev/null 2>&1; then
|
|
459
|
+
pt_log "No suitable Python interpreter found — skipping venv provisioning"
|
|
239
460
|
append_env "HARNESS_ECOSYSTEM" "python"
|
|
240
461
|
append_env "PYTHON_BIN" "${PYTHON_BIN:-python3}"
|
|
241
462
|
return
|
|
242
463
|
fi
|
|
243
464
|
|
|
465
|
+
if [ -n "$project_min" ]; then
|
|
466
|
+
har_python_warn_if_below_minimum "$interpreter" "$project_min"
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
if [ -d "$venv_dir" ] && [ -x "$venv_dir/bin/python" ] && [ -n "$project_min" ]; then
|
|
470
|
+
local venv_ver=""
|
|
471
|
+
venv_ver="$(har_python_version_from_bin "$venv_dir/bin/python")"
|
|
472
|
+
if [ -n "$venv_ver" ] && ! har_python_version_ge "$venv_ver" "$project_min"; then
|
|
473
|
+
pt_log "Existing venv Python ${venv_ver} is older than required ${project_min} — recreating..."
|
|
474
|
+
rm -rf "$venv_dir"
|
|
475
|
+
fi
|
|
476
|
+
fi
|
|
477
|
+
|
|
244
478
|
if [ ! -d "$venv_dir" ]; then
|
|
245
|
-
|
|
246
|
-
if ! python3 -m venv "$venv_dir"; then
|
|
479
|
+
if ! har_python_create_venv "$dir" "$venv_dir" "$interpreter"; then
|
|
247
480
|
rm -rf "$venv_dir"
|
|
248
|
-
pt_log "
|
|
481
|
+
pt_log "venv creation failed (on Debian/Ubuntu install python3-venv) — using resolved interpreter"
|
|
249
482
|
append_env "HARNESS_ECOSYSTEM" "python"
|
|
250
|
-
append_env "PYTHON_BIN" "$
|
|
483
|
+
append_env "PYTHON_BIN" "$interpreter"
|
|
251
484
|
return
|
|
252
485
|
fi
|
|
253
486
|
fi
|
|
254
487
|
|
|
255
488
|
if [ ! -x "$venv_dir/bin/python" ]; then
|
|
256
|
-
pt_log "Python venv at $venv_rel is missing or broken — using
|
|
489
|
+
pt_log "Python venv at $venv_rel is missing or broken — using resolved interpreter"
|
|
257
490
|
append_env "HARNESS_ECOSYSTEM" "python"
|
|
258
|
-
append_env "PYTHON_BIN" "$
|
|
491
|
+
append_env "PYTHON_BIN" "$interpreter"
|
|
259
492
|
return
|
|
260
493
|
fi
|
|
261
494
|
|
|
262
495
|
python_bin="$venv_dir/bin/python"
|
|
496
|
+
if [ -n "$project_min" ]; then
|
|
497
|
+
har_python_warn_if_below_minimum "$python_bin" "$project_min"
|
|
498
|
+
fi
|
|
499
|
+
|
|
263
500
|
# shellcheck disable=SC1091
|
|
264
501
|
source "$venv_dir/bin/activate"
|
|
265
502
|
|
|
266
503
|
if ! run_install_cmd "$dir"; then
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
504
|
+
if ! har_python_install_deps "$dir" "$venv_dir"; then
|
|
505
|
+
pt_log "Installing Python dependencies..."
|
|
506
|
+
if [ -f "$dir/pyproject.toml" ]; then
|
|
507
|
+
(cd "$dir" && pip install -q -e ".[dev]" 2>/dev/null) || (cd "$dir" && pip install -q -e .)
|
|
508
|
+
elif [ -f "$dir/requirements.txt" ]; then
|
|
509
|
+
(cd "$dir" && pip install -q -r requirements.txt)
|
|
510
|
+
elif [ -f "$dir/setup.py" ] || [ -f "$dir/setup.cfg" ]; then
|
|
511
|
+
(cd "$dir" && pip install -q -e .)
|
|
512
|
+
elif [ -f "$dir/Pipfile" ] && command -v pipenv >/dev/null 2>&1; then
|
|
513
|
+
(cd "$dir" && pipenv install --dev)
|
|
514
|
+
python_bin="$(cd "$dir" && pipenv --py)"
|
|
515
|
+
fi
|
|
277
516
|
fi
|
|
278
517
|
fi
|
|
279
518
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Verification pipeline for iOS mobile app repos — build and test with xcodebuild.
|
|
3
|
-
# Outputs JSON to stdout, human-readable progress to stderr.
|
|
3
|
+
# Outputs JSON to stdout (machine contract), human-readable progress to stderr.
|
|
4
|
+
# Passing steps omit `output`. `har env verify` streams progress only.
|
|
4
5
|
#
|
|
5
6
|
# Usage: ./.har/verify.sh <agent-id> [--full]
|
|
6
7
|
#
|
|
@@ -92,7 +93,7 @@ run_step() {
|
|
|
92
93
|
end=$(now_ms)
|
|
93
94
|
elapsed=$(( end - start ))
|
|
94
95
|
|
|
95
|
-
local pass_bool
|
|
96
|
+
local pass_bool
|
|
96
97
|
if [ "$exit_code" = "0" ]; then
|
|
97
98
|
echo "✓ (${elapsed}ms)" >&2
|
|
98
99
|
pass_bool="true"
|
|
@@ -103,14 +104,7 @@ run_step() {
|
|
|
103
104
|
OVERALL_PASS=false
|
|
104
105
|
fi
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
RESULTS_JSON=$(echo "$RESULTS_JSON" | node -e "
|
|
109
|
-
const fs = require('fs');
|
|
110
|
-
let arr = JSON.parse(fs.readFileSync('/dev/stdin','utf8'));
|
|
111
|
-
arr.push({name:'$name',pass:$pass_bool,ms:$elapsed,output:$step_output_escaped});
|
|
112
|
-
process.stdout.write(JSON.stringify(arr));
|
|
113
|
-
" 2>/dev/null || echo "$RESULTS_JSON")
|
|
107
|
+
record_step_result "$name" "$pass_bool" "$elapsed" "$output"
|
|
114
108
|
|
|
115
109
|
if [ "$pass_bool" = "false" ] && [ -z "$FULL" ]; then
|
|
116
110
|
return 1
|
|
@@ -228,52 +228,291 @@ provision_node() {
|
|
|
228
228
|
append_path_prefix "$dir/node_modules/.bin"
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
# ── Python interpreter helpers ────────────────────────────────────────────────
|
|
232
|
+
# Resolve the project Python (requires-python, .python-version, uv) instead of
|
|
233
|
+
# blindly using PATH python3.
|
|
234
|
+
|
|
235
|
+
har_python_version_from_bin() {
|
|
236
|
+
local bin="$1"
|
|
237
|
+
"$bin" -c 'import sys; print(".".join(map(str, sys.version_info[:3])))' 2>/dev/null \
|
|
238
|
+
| tr -d '[:space:]'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
har_python_version_to_sortable() {
|
|
242
|
+
local v="${1#python}"
|
|
243
|
+
v="${v#v}"
|
|
244
|
+
local major=0 minor=0 patch=0
|
|
245
|
+
IFS='.' read -r major minor patch _ <<< "$v"
|
|
246
|
+
major=${major:-0}
|
|
247
|
+
minor=${minor:-0}
|
|
248
|
+
patch=${patch:-0}
|
|
249
|
+
printf '%03d%03d%03d' "$major" "$minor" "$patch"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
har_python_version_ge() {
|
|
253
|
+
local a b
|
|
254
|
+
a="$(har_python_version_to_sortable "$1")"
|
|
255
|
+
b="$(har_python_version_to_sortable "$2")"
|
|
256
|
+
[ "$a" -ge "$b" ]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
har_python_requires_minimum() {
|
|
260
|
+
local spec="$1"
|
|
261
|
+
local ver=""
|
|
262
|
+
|
|
263
|
+
[ -n "$spec" ] || return 1
|
|
264
|
+
|
|
265
|
+
ver="$(printf '%s' "$spec" | sed -n \
|
|
266
|
+
-e 's/.*>=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
|
|
267
|
+
-e 's/.*~=\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
|
|
268
|
+
-e 's/.*==\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
|
|
269
|
+
| head -1)"
|
|
270
|
+
|
|
271
|
+
if [ -z "$ver" ]; then
|
|
272
|
+
ver="$(printf '%s' "$spec" | sed -n 's/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p')"
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
[ -n "$ver" ] && echo "$ver"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
har_python_read_dot_version() {
|
|
279
|
+
local dir="$1"
|
|
280
|
+
[ -f "$dir/.python-version" ] || return 1
|
|
281
|
+
head -1 "$dir/.python-version" | tr -d '[:space:]'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
har_python_read_requires_spec() {
|
|
285
|
+
local dir="$1"
|
|
286
|
+
local spec=""
|
|
287
|
+
|
|
288
|
+
if [ -f "$dir/pyproject.toml" ]; then
|
|
289
|
+
spec="$(sed -n 's/^[[:space:]]*requires-python[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
|
|
290
|
+
"$dir/pyproject.toml" | head -1)"
|
|
291
|
+
fi
|
|
292
|
+
if [ -z "$spec" ] && [ -f "$dir/setup.cfg" ]; then
|
|
293
|
+
spec="$(sed -n '/^\[options\]/,/^\[/ s/^python_requires[[:space:]]*=[[:space:]]*\(.*\)/\1/p' \
|
|
294
|
+
"$dir/setup.cfg" | head -1 | tr -d ' "')"
|
|
295
|
+
fi
|
|
296
|
+
[ -n "$spec" ] && echo "$spec"
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
har_python_project_minimum() {
|
|
300
|
+
local dir="$1"
|
|
301
|
+
local req_min="" dot_ver="" higher=""
|
|
302
|
+
|
|
303
|
+
req_min="$(har_python_requires_minimum "$(har_python_read_requires_spec "$dir" || true)" || true)"
|
|
304
|
+
dot_ver="$(har_python_read_dot_version "$dir" || true)"
|
|
305
|
+
if [ -n "$dot_ver" ]; then
|
|
306
|
+
dot_ver="$(printf '%s' "$dot_ver" | sed 's/^v//; s/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/')"
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
if [ -n "$req_min" ] && [ -n "$dot_ver" ]; then
|
|
310
|
+
if har_python_version_ge "$dot_ver" "$req_min"; then
|
|
311
|
+
higher="$dot_ver"
|
|
312
|
+
else
|
|
313
|
+
higher="$req_min"
|
|
314
|
+
fi
|
|
315
|
+
elif [ -n "$req_min" ]; then
|
|
316
|
+
higher="$req_min"
|
|
317
|
+
elif [ -n "$dot_ver" ]; then
|
|
318
|
+
higher="$dot_ver"
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
[ -n "$higher" ] && echo "$higher"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
har_python_is_uv_project() {
|
|
325
|
+
local dir="$1"
|
|
326
|
+
[ -f "$dir/uv.lock" ] && return 0
|
|
327
|
+
[ -f "$dir/pyproject.toml" ] && grep -q '^\[tool\.uv\]' "$dir/pyproject.toml" 2>/dev/null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
har_python_uv_find() {
|
|
331
|
+
local dir="$1"
|
|
332
|
+
local request="${2:-}"
|
|
333
|
+
|
|
334
|
+
command -v uv >/dev/null 2>&1 || return 1
|
|
335
|
+
if [ -n "$request" ]; then
|
|
336
|
+
(cd "$dir" && uv python find "$request" 2>/dev/null)
|
|
337
|
+
else
|
|
338
|
+
(cd "$dir" && uv python find 2>/dev/null)
|
|
339
|
+
fi
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
har_python_resolve_interpreter() {
|
|
343
|
+
local dir="$1"
|
|
344
|
+
local minimum="${2:-}"
|
|
345
|
+
local found=""
|
|
346
|
+
|
|
347
|
+
if har_python_is_uv_project "$dir"; then
|
|
348
|
+
found="$(har_python_uv_find "$dir" "$minimum" || true)"
|
|
349
|
+
if [ -n "$found" ] && [ -x "$found" ]; then
|
|
350
|
+
echo "$found"
|
|
351
|
+
return 0
|
|
352
|
+
fi
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
if [ -f "$dir/.python-version" ]; then
|
|
356
|
+
found="$(har_python_uv_find "$dir" "$(har_python_read_dot_version "$dir")" || true)"
|
|
357
|
+
if [ -n "$found" ] && [ -x "$found" ]; then
|
|
358
|
+
echo "$found"
|
|
359
|
+
return 0
|
|
360
|
+
fi
|
|
361
|
+
if command -v pyenv >/dev/null 2>&1; then
|
|
362
|
+
found="$(cd "$dir" && pyenv which python 2>/dev/null || true)"
|
|
363
|
+
if [ -n "$found" ] && [ -x "$found" ]; then
|
|
364
|
+
echo "$found"
|
|
365
|
+
return 0
|
|
366
|
+
fi
|
|
367
|
+
fi
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
if [ -n "$minimum" ]; then
|
|
371
|
+
found="$(har_python_uv_find "$dir" "$minimum" || true)"
|
|
372
|
+
if [ -n "$found" ] && [ -x "$found" ]; then
|
|
373
|
+
echo "$found"
|
|
374
|
+
return 0
|
|
375
|
+
fi
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
379
|
+
local sys_py sys_ver=""
|
|
380
|
+
sys_py="$(command -v python3)"
|
|
381
|
+
if [ -z "$minimum" ]; then
|
|
382
|
+
echo "$sys_py"
|
|
383
|
+
return 0
|
|
384
|
+
fi
|
|
385
|
+
sys_ver="$(har_python_version_from_bin "$sys_py")"
|
|
386
|
+
if [ -n "$sys_ver" ] && har_python_version_ge "$sys_ver" "$minimum"; then
|
|
387
|
+
echo "$sys_py"
|
|
388
|
+
return 0
|
|
389
|
+
fi
|
|
390
|
+
echo "$sys_py"
|
|
391
|
+
return 0
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
return 1
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
har_python_warn_if_below_minimum() {
|
|
398
|
+
local bin="$1"
|
|
399
|
+
local minimum="$2"
|
|
400
|
+
local actual=""
|
|
401
|
+
|
|
402
|
+
[ -n "$minimum" ] || return 0
|
|
403
|
+
actual="$(har_python_version_from_bin "$bin")"
|
|
404
|
+
[ -n "$actual" ] || return 0
|
|
405
|
+
if ! har_python_version_ge "$actual" "$minimum"; then
|
|
406
|
+
pt_log "WARNING: resolved Python ${actual} is older than required ${minimum} (requires-python / .python-version)"
|
|
407
|
+
fi
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
har_python_create_venv() {
|
|
411
|
+
local dir="$1"
|
|
412
|
+
local venv_dir="$2"
|
|
413
|
+
local interpreter="$3"
|
|
414
|
+
local venv_rel="${venv_dir#"$dir"/}"
|
|
415
|
+
|
|
416
|
+
if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
|
|
417
|
+
pt_log "Creating Python venv with uv at ${venv_rel}..."
|
|
418
|
+
if (cd "$dir" && uv venv --python "$interpreter" "$venv_dir" 2>/dev/null) \
|
|
419
|
+
|| (cd "$dir" && uv venv "$venv_dir" 2>/dev/null); then
|
|
420
|
+
return 0
|
|
421
|
+
fi
|
|
422
|
+
pt_log "uv venv failed — falling back to ${interpreter} -m venv"
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
pt_log "Creating Python venv at ${venv_rel}..."
|
|
426
|
+
"$interpreter" -m venv "$venv_dir"
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
har_python_install_deps() {
|
|
430
|
+
local dir="$1"
|
|
431
|
+
local venv_dir="$2"
|
|
432
|
+
|
|
433
|
+
if har_python_is_uv_project "$dir" && command -v uv >/dev/null 2>&1; then
|
|
434
|
+
pt_log "Syncing Python dependencies with uv..."
|
|
435
|
+
if (cd "$dir" && UV_PROJECT_ENVIRONMENT="$venv_dir" uv sync --quiet 2>/dev/null); then
|
|
436
|
+
return 0
|
|
437
|
+
fi
|
|
438
|
+
if (cd "$dir" && uv sync --quiet 2>/dev/null); then
|
|
439
|
+
return 0
|
|
440
|
+
fi
|
|
441
|
+
pt_log "uv sync failed — falling back to pip"
|
|
442
|
+
return 1
|
|
443
|
+
fi
|
|
444
|
+
return 1
|
|
445
|
+
}
|
|
446
|
+
|
|
231
447
|
provision_python() {
|
|
232
448
|
local dir="$1"
|
|
233
449
|
local venv_rel="${HARNESS_PYTHON_VENV_DIR:-.har/venv}"
|
|
234
450
|
local venv_dir="$dir/$venv_rel"
|
|
235
|
-
local python_bin="
|
|
451
|
+
local python_bin=""
|
|
452
|
+
local interpreter=""
|
|
453
|
+
local project_min=""
|
|
454
|
+
|
|
455
|
+
project_min="$(har_python_project_minimum "$dir" || true)"
|
|
456
|
+
interpreter="$(har_python_resolve_interpreter "$dir" "$project_min" || true)"
|
|
236
457
|
|
|
237
|
-
if ! command -v
|
|
238
|
-
pt_log "
|
|
458
|
+
if [ -z "$interpreter" ] || ! command -v "$interpreter" >/dev/null 2>&1; then
|
|
459
|
+
pt_log "No suitable Python interpreter found — skipping venv provisioning"
|
|
239
460
|
append_env "HARNESS_ECOSYSTEM" "python"
|
|
240
461
|
append_env "PYTHON_BIN" "${PYTHON_BIN:-python3}"
|
|
241
462
|
return
|
|
242
463
|
fi
|
|
243
464
|
|
|
465
|
+
if [ -n "$project_min" ]; then
|
|
466
|
+
har_python_warn_if_below_minimum "$interpreter" "$project_min"
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
if [ -d "$venv_dir" ] && [ -x "$venv_dir/bin/python" ] && [ -n "$project_min" ]; then
|
|
470
|
+
local venv_ver=""
|
|
471
|
+
venv_ver="$(har_python_version_from_bin "$venv_dir/bin/python")"
|
|
472
|
+
if [ -n "$venv_ver" ] && ! har_python_version_ge "$venv_ver" "$project_min"; then
|
|
473
|
+
pt_log "Existing venv Python ${venv_ver} is older than required ${project_min} — recreating..."
|
|
474
|
+
rm -rf "$venv_dir"
|
|
475
|
+
fi
|
|
476
|
+
fi
|
|
477
|
+
|
|
244
478
|
if [ ! -d "$venv_dir" ]; then
|
|
245
|
-
|
|
246
|
-
if ! python3 -m venv "$venv_dir"; then
|
|
479
|
+
if ! har_python_create_venv "$dir" "$venv_dir" "$interpreter"; then
|
|
247
480
|
rm -rf "$venv_dir"
|
|
248
|
-
pt_log "
|
|
481
|
+
pt_log "venv creation failed (on Debian/Ubuntu install python3-venv) — using resolved interpreter"
|
|
249
482
|
append_env "HARNESS_ECOSYSTEM" "python"
|
|
250
|
-
append_env "PYTHON_BIN" "$
|
|
483
|
+
append_env "PYTHON_BIN" "$interpreter"
|
|
251
484
|
return
|
|
252
485
|
fi
|
|
253
486
|
fi
|
|
254
487
|
|
|
255
488
|
if [ ! -x "$venv_dir/bin/python" ]; then
|
|
256
|
-
pt_log "Python venv at $venv_rel is missing or broken — using
|
|
489
|
+
pt_log "Python venv at $venv_rel is missing or broken — using resolved interpreter"
|
|
257
490
|
append_env "HARNESS_ECOSYSTEM" "python"
|
|
258
|
-
append_env "PYTHON_BIN" "$
|
|
491
|
+
append_env "PYTHON_BIN" "$interpreter"
|
|
259
492
|
return
|
|
260
493
|
fi
|
|
261
494
|
|
|
262
495
|
python_bin="$venv_dir/bin/python"
|
|
496
|
+
if [ -n "$project_min" ]; then
|
|
497
|
+
har_python_warn_if_below_minimum "$python_bin" "$project_min"
|
|
498
|
+
fi
|
|
499
|
+
|
|
263
500
|
# shellcheck disable=SC1091
|
|
264
501
|
source "$venv_dir/bin/activate"
|
|
265
502
|
|
|
266
503
|
if ! run_install_cmd "$dir"; then
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
504
|
+
if ! har_python_install_deps "$dir" "$venv_dir"; then
|
|
505
|
+
pt_log "Installing Python dependencies..."
|
|
506
|
+
if [ -f "$dir/pyproject.toml" ]; then
|
|
507
|
+
(cd "$dir" && pip install -q -e ".[dev]" 2>/dev/null) || (cd "$dir" && pip install -q -e .)
|
|
508
|
+
elif [ -f "$dir/requirements.txt" ]; then
|
|
509
|
+
(cd "$dir" && pip install -q -r requirements.txt)
|
|
510
|
+
elif [ -f "$dir/setup.py" ] || [ -f "$dir/setup.cfg" ]; then
|
|
511
|
+
(cd "$dir" && pip install -q -e .)
|
|
512
|
+
elif [ -f "$dir/Pipfile" ] && command -v pipenv >/dev/null 2>&1; then
|
|
513
|
+
(cd "$dir" && pipenv install --dev)
|
|
514
|
+
python_bin="$(cd "$dir" && pipenv --py)"
|
|
515
|
+
fi
|
|
277
516
|
fi
|
|
278
517
|
fi
|
|
279
518
|
|