Nix packages for third-party projects that don't ship their own flakes
0

Configure Feed

Select the types of activity you want to include in your feed.

agentic / scripts / update-packages.sh
19 kB 623 lines
1#!/usr/bin/env bash 2# Update packages/* from their upstream sources. 3# Intended for local use and weekly CI (see .github/workflows/update-packages.yml). 4# 5# Usage: 6# scripts/update-packages.sh # update all packages that have upstream.json 7# scripts/update-packages.sh vit # update only vit 8# scripts/update-packages.sh --check # report outdated packages; exit 1 if any 9set -euo pipefail 10 11ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 12cd "$ROOT" 13 14FAKE_HASH="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" 15CHECK_ONLY=0 16PACKAGES=() 17 18log() { printf '==> %s\n' "$*"; } 19warn() { printf 'warning: %s\n' "$*" >&2; } 20die() { printf 'error: %s\n' "$*" >&2; exit 1; } 21 22need() { 23 command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" 24} 25 26for arg in "$@"; do 27 case "$arg" in 28 --check) CHECK_ONLY=1 ;; 29 -h|--help) 30 sed -n '2,10p' "$0" 31 exit 0 32 ;; 33 -*) 34 die "unknown flag: $arg" 35 ;; 36 *) 37 PACKAGES+=("$arg") 38 ;; 39 esac 40done 41 42need curl 43need nix 44need nix-prefetch-url 45need python3 46 47if [[ ${#PACKAGES[@]} -eq 0 ]]; then 48 while IFS= read -r d; do 49 name="$(basename "$d")" 50 [[ -f "$d/upstream.json" && -f "$d/default.nix" ]] || continue 51 PACKAGES+=("$name") 52 done < <(find "$ROOT/packages" -mindepth 1 -maxdepth 1 -type d | sort) 53fi 54 55[[ ${#PACKAGES[@]} -gt 0 ]] || die "no packages to update" 56 57read_field() { 58 # read_field <file> <json-key> 59 python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2],""))' "$1" "$2" 60} 61 62current_version() { 63 python3 -c ' 64import re, sys 65text = open(sys.argv[1]).read() 66m = re.search(r"version\s*=\s*\"([^\"]+)\"", text) 67if not m: 68 sys.exit("version not found in " + sys.argv[1]) 69print(m.group(1)) 70' "$1" 71} 72 73set_field_string() { 74 # set_field_string <file> <attr> <value> — replaces first attr = "..." 75 local file="$1" attr="$2" value="$3" 76 python3 -c ' 77import re, sys 78path, attr, value = sys.argv[1], sys.argv[2], sys.argv[3] 79text = open(path).read() 80pat = re.compile(rf"({re.escape(attr)}\s*=\s*\")([^\"]*)(\")", re.M) 81new, n = pat.subn(rf"\g<1>{value}\g<3>", text, count=1) 82if n != 1: 83 sys.exit(f"failed to set {attr} in {path} (matches={n})") 84open(path, "w").write(new) 85' "$file" "$attr" "$value" 86} 87 88set_npm_deps_hash() { 89 local file="$1" value="$2" 90 python3 -c ' 91import re, sys 92path, value = sys.argv[1], sys.argv[2] 93text = open(path).read() 94pat = re.compile(r"(npmDepsHash\s*=\s*)([^;]+)(;)") 95new, n = pat.subn(rf"\g<1>\"{value}\"\g<3>", text, count=1) 96if n != 1: 97 sys.exit(f"failed to set npmDepsHash in {path} (matches={n})") 98open(path, "w").write(new) 99' "$file" "$value" 100} 101 102sri_from_url_unpack() { 103 local url="$1" 104 local base32 105 base32="$(nix-prefetch-url --unpack "$url" 2>/dev/null)" 106 nix hash convert --hash-algo sha256 --to sri "$base32" 107} 108 109latest_github_tag() { 110 # latest_github_tag <owner/repo> <prefix> 111 local repo="$1" prefix="$2" 112 local url="https://api.github.com/repos/${repo}/tags?per_page=30" 113 local args=(-fsSL) 114 if [[ -n "${GITHUB_TOKEN:-}" ]]; then 115 args+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") 116 fi 117 curl "${args[@]}" "$url" | python3 -c ' 118import json, re, sys 119prefix = sys.argv[1] 120tags = json.load(sys.stdin) 121# Prefer semver-ish tags matching prefix 122pat = re.compile(r"^" + re.escape(prefix) + r"(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$") 123versions = [] 124for t in tags: 125 name = t.get("name") or "" 126 m = pat.match(name) 127 if m: 128 versions.append((m.group(1), name)) 129if not versions: 130 sys.exit("no matching tags for prefix " + prefix) 131# sort by version components 132def key(v): 133 ver = re.split(r"[+-]", v[0], maxsplit=1)[0] 134 return [int(x) for x in ver.split(".")] 135versions.sort(key=key) 136print(versions[-1][0]) 137' "$prefix" 138} 139 140version_gt() { 141 # version_gt a b => true if a > b (semver-ish, numeric) 142 python3 -c ' 143import re, sys 144def parts(v): 145 core = re.split(r"[+-]", v, maxsplit=1)[0] 146 return [int(x) for x in core.split(".")] 147a, b = sys.argv[1], sys.argv[2] 148sys.exit(0 if parts(a) > parts(b) else 1) 149' "$1" "$2" 150} 151 152capture_npm_deps_hash() { 153 # Build with fake hash; parse the "got:" line from the FOD mismatch. 154 local attr="$1" 155 local log 156 log="$(mktemp)" 157 set +e 158 nix build "path:${ROOT}#${attr}" --print-build-logs >"$log" 2>&1 159 local status=$? 160 set -e 161 local got 162 got="$(python3 -c ' 163import re, sys 164text = open(sys.argv[1]).read() 165# Prefer the last "got:" line (npm-deps FOD). 166matches = re.findall(r"got:\s+(sha256-[A-Za-z0-9+/=]+)", text) 167if not matches: 168 sys.exit(1) 169print(matches[-1]) 170' "$log" 2>/dev/null || true)" 171 if [[ -z "$got" ]]; then 172 warn "failed to capture npmDepsHash for ${attr}; build log:" 173 tail -n 80 "$log" >&2 || true 174 rm -f "$log" 175 return 1 176 fi 177 rm -f "$log" 178 printf '%s\n' "$got" 179 return 0 180} 181 182verify_build() { 183 local attr="$1" 184 log "verifying nix build .#${attr}" 185 nix build "path:${ROOT}#${attr}" --print-build-logs 186} 187 188regenerate_npm_lock() { 189 # regenerate_npm_lock <owner/repo> <version> <dest-lock> 190 local repo="$1" version="$2" dest="$3" 191 need npm 192 local tmp archive dir 193 tmp="$(mktemp -d)" 194 archive="${tmp}/src.tar.gz" 195 dir="${tmp}/src" 196 curl -fsSL "https://github.com/${repo}/archive/refs/tags/v${version}.tar.gz" -o "$archive" 197 mkdir -p "$dir" 198 tar -xzf "$archive" -C "$dir" --strip-components=1 199 ( 200 cd "$dir" 201 rm -f package-lock.json bun.lock 202 npm install --package-lock-only --ignore-scripts 203 ) 204 cp "$dir/package-lock.json" "$dest" 205 rm -rf "$tmp" 206} 207 208set_fetch_from_github_field() { 209 # set_fetch_from_github_field <file> <attr> <value> 210 # Replaces attr = "..." inside the first fetchFromGitHub { ... } block. 211 local file="$1" attr="$2" value="$3" 212 python3 -c ' 213import re, sys 214path, attr, value = sys.argv[1], sys.argv[2], sys.argv[3] 215text = open(path).read() 216pat = re.compile( 217 rf"(fetchFromGitHub\s*\{{[^}}]*?{re.escape(attr)}\s*=\s*\")([^\"]+)(\")", 218 re.S, 219) 220new, n = pat.subn(rf"\g<1>{value}\g<3>", text, count=1) 221if n != 1: 222 sys.exit(f"failed to set fetchFromGitHub.{attr} in {path} (matches={n})") 223open(path, "w").write(new) 224' "$file" "$attr" "$value" 225} 226 227current_rev() { 228 python3 -c ' 229import re, sys 230text = open(sys.argv[1]).read() 231m = re.search(r"fetchFromGitHub\s*\{[^}]*?rev\s*=\s*\"([^\"]+)\"", text, re.S) 232if not m: 233 sys.exit("rev not found in " + sys.argv[1]) 234print(m.group(1)) 235' "$1" 236} 237 238latest_github_commit() { 239 # latest_github_commit <owner/repo> <branch> 240 # prints: <sha> <YYYY-MM-DD> 241 local repo="$1" branch="$2" 242 local url="https://api.github.com/repos/${repo}/commits/${branch}" 243 local args=(-fsSL) 244 if [[ -n "${GITHUB_TOKEN:-}" ]]; then 245 args+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") 246 fi 247 curl "${args[@]}" "$url" | python3 -c ' 248import json, sys 249c = json.load(sys.stdin) 250sha = c["sha"] 251date = c["commit"]["committer"]["date"][:10] 252print(sha, date) 253' 254} 255 256sri_from_url_file() { 257 local url="$1" 258 # nix-prefetch-url (no --unpack) for plain files (gems, etc.) 259 local base32 260 base32="$(nix-prefetch-url "$url" 2>/dev/null)" 261 nix hash convert --hash-algo sha256 --to sri "$base32" 262} 263 264refresh_vendored_gems() { 265 # refresh_vendored_gems <default.nix> <owner/repo> <rev> 266 # If the package vendors prism/rbs gems, re-read versions from upstream 267 # Makefile at that rev and refresh fetchurl hashes when they change. 268 local default_nix="$1" repo="$2" rev="$3" 269 python3 -c ' 270import re, sys 271text = open(sys.argv[1]).read() 272sys.exit(0 if re.search(r"prismVersion\s*=", text) else 1) 273' "$default_nix" 2>/dev/null || return 0 274 275 local makefile 276 makefile="$(mktemp)" 277 local args=(-fsSL) 278 if [[ -n "${GITHUB_TOKEN:-}" ]]; then 279 args+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") 280 fi 281 curl "${args[@]}" \ 282 "https://raw.githubusercontent.com/${repo}/${rev}/Makefile" \ 283 -o "$makefile" 284 285 local prism_ver rbs_ver 286 prism_ver="$(python3 -c ' 287import re, sys 288text = open(sys.argv[1]).read() 289m = re.search(r"PRISM_VERSION\s*\?=\s*(\S+)", text) 290print(m.group(1) if m else "") 291' "$makefile")" 292 rbs_ver="$(python3 -c ' 293import re, sys 294text = open(sys.argv[1]).read() 295m = re.search(r"RBS_VERSION\s*\?=\s*(\S+)", text) 296print(m.group(1) if m else "") 297' "$makefile")" 298 rm -f "$makefile" 299 300 [[ -n "$prism_ver" && -n "$rbs_ver" ]] || { 301 warn "could not parse PRISM_VERSION/RBS_VERSION from upstream Makefile" 302 return 0 303 } 304 305 local cur_prism cur_rbs 306 cur_prism="$(python3 -c ' 307import re, sys 308m = re.search(r"prismVersion\s*=\s*\"([^\"]+)\"", open(sys.argv[1]).read()) 309print(m.group(1) if m else "") 310' "$default_nix")" 311 cur_rbs="$(python3 -c ' 312import re, sys 313m = re.search(r"rbsVersion\s*=\s*\"([^\"]+)\"", open(sys.argv[1]).read()) 314print(m.group(1) if m else "") 315' "$default_nix")" 316 317 if [[ "$prism_ver" != "$cur_prism" ]]; then 318 log " prism ${cur_prism} -> ${prism_ver}" 319 set_field_string "$default_nix" prismVersion "$prism_ver" 320 local prism_hash 321 prism_hash="$(sri_from_url_file "https://rubygems.org/gems/prism-${prism_ver}.gem")" 322 python3 -c ' 323import re, sys 324path, ver, h = sys.argv[1], sys.argv[2], sys.argv[3] 325text = open(path).read() 326# first fetchurl after prismGem = 327pat = re.compile( 328 r"(prismGem\s*=\s*fetchurl\s*\{[^}]*?hash\s*=\s*\")([^\"]+)(\")", 329 re.S, 330) 331new, n = pat.subn(rf"\g<1>{h}\g<3>", text, count=1) 332if n != 1: 333 sys.exit(f"failed to set prismGem hash (matches={n})") 334open(path, "w").write(new) 335' "$default_nix" "$prism_ver" "$prism_hash" 336 log " prism gem hash ${prism_hash}" 337 fi 338 339 if [[ "$rbs_ver" != "$cur_rbs" ]]; then 340 log " rbs ${cur_rbs} -> ${rbs_ver}" 341 set_field_string "$default_nix" rbsVersion "$rbs_ver" 342 local rbs_hash 343 rbs_hash="$(sri_from_url_file "https://rubygems.org/gems/rbs-${rbs_ver}.gem")" 344 python3 -c ' 345import re, sys 346path, h = sys.argv[1], sys.argv[2] 347text = open(path).read() 348pat = re.compile( 349 r"(rbsGem\s*=\s*fetchurl\s*\{[^}]*?hash\s*=\s*\")([^\"]+)(\")", 350 re.S, 351) 352new, n = pat.subn(rf"\g<1>{h}\g<3>", text, count=1) 353if n != 1: 354 sys.exit(f"failed to set rbsGem hash (matches={n})") 355open(path, "w").write(new) 356' "$default_nix" "$rbs_hash" 357 log " rbs gem hash ${rbs_hash}" 358 fi 359} 360 361update_npm_github() { 362 local name="$1" 363 local pkg_dir="$ROOT/packages/${name}" 364 local default_nix="$pkg_dir/default.nix" 365 local upstream="$pkg_dir/upstream.json" 366 local repo tag_prefix 367 repo="$(read_field "$upstream" github)" 368 tag_prefix="$(read_field "$upstream" tag_prefix)" 369 [[ -n "$repo" ]] || die "${name}: upstream.json missing github" 370 tag_prefix="${tag_prefix:-v}" 371 372 local cur latest 373 cur="$(current_version "$default_nix")" 374 log "${name}: current=${cur}" 375 latest="$(latest_github_tag "$repo" "$tag_prefix")" 376 log "${name}: latest upstream=${latest}" 377 378 if ! version_gt "$latest" "$cur"; then 379 log "${name}: already up to date" 380 return 0 381 fi 382 383 if [[ "$CHECK_ONLY" -eq 1 ]]; then 384 log "${name}: OUTDATED (${cur} -> ${latest})" 385 return 10 386 fi 387 388 log "${name}: updating ${cur} -> ${latest}" 389 set_field_string "$default_nix" version "$latest" 390 391 local src_url src_hash 392 src_url="https://github.com/${repo}/archive/refs/tags/${tag_prefix}${latest}.tar.gz" 393 log "${name}: prefetching src ${src_url}" 394 src_hash="$(sri_from_url_unpack "$src_url")" 395 set_fetch_from_github_field "$default_nix" hash "$src_hash" 396 log "${name}: src hash ${src_hash}" 397 398 if [[ -f "$pkg_dir/package-lock.json" ]]; then 399 log "${name}: regenerating package-lock.json" 400 regenerate_npm_lock "$repo" "$latest" "$pkg_dir/package-lock.json" 401 fi 402 403 log "${name}: computing npmDepsHash" 404 set_npm_deps_hash "$default_nix" "$FAKE_HASH" 405 local npm_hash 406 npm_hash="$(capture_npm_deps_hash "$name")" 407 set_npm_deps_hash "$default_nix" "$npm_hash" 408 log "${name}: npmDepsHash ${npm_hash}" 409 410 verify_build "$name" 411 log "${name}: updated successfully to ${latest}" 412} 413 414latest_github_tag_with_suffix() { 415 # latest_github_tag_with_suffix <owner/repo> <prefix> <suffix> 416 # e.g. prefix=v suffix=-pre => matches v1.12.0-pre, prints 1.12.0-pre 417 local repo="$1" prefix="$2" suffix="$3" 418 local url="https://api.github.com/repos/${repo}/releases?per_page=30" 419 local args=(-fsSL) 420 if [[ -n "${GITHUB_TOKEN:-}" ]]; then 421 args+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") 422 fi 423 curl "${args[@]}" "$url" | python3 -c ' 424import json, re, sys 425prefix, suffix = sys.argv[1], sys.argv[2] 426releases = json.load(sys.stdin) 427pat = re.compile( 428 r"^" + re.escape(prefix) + r"(\d+\.\d+\.\d+)" + re.escape(suffix) + r"$" 429) 430versions = [] 431for r in releases: 432 if r.get("draft"): 433 continue 434 name = r.get("tag_name") or "" 435 m = pat.match(name) 436 if m: 437 versions.append(m.group(1) + suffix) 438if not versions: 439 sys.exit("no matching release tags for prefix=%r suffix=%r" % (prefix, suffix)) 440def key(v): 441 core = re.split(r"[+-]", v, maxsplit=1)[0] 442 return [int(x) for x in core.split(".")] 443versions.sort(key=key) 444print(versions[-1]) 445' "$prefix" "$suffix" 446} 447 448set_fetchurl_hash() { 449 # set_fetchurl_hash <file> <value> 450 # Replaces hash = "..." inside the first fetchurl { ... } block. 451 local file="$1" value="$2" 452 python3 -c ' 453import re, sys 454path, value = sys.argv[1], sys.argv[2] 455text = open(path).read() 456pat = re.compile( 457 r"(fetchurl\s*\{[^}]*?hash\s*=\s*\")([^\"]+)(\")", 458 re.S, 459) 460new, n = pat.subn(rf"\g<1>{value}\g<3>", text, count=1) 461if n != 1: 462 sys.exit(f"failed to set fetchurl.hash in {path} (matches={n})") 463open(path, "w").write(new) 464' "$file" "$value" 465} 466 467update_github_release_binary() { 468 # Official prebuilt release assets (e.g. zed-preview Linux tarball). 469 # version in default.nix is the tag without the leading "v" (e.g. 1.12.0-pre). 470 local name="$1" 471 local pkg_dir="$ROOT/packages/${name}" 472 local default_nix="$pkg_dir/default.nix" 473 local upstream="$pkg_dir/upstream.json" 474 local repo tag_prefix tag_suffix asset 475 repo="$(read_field "$upstream" github)" 476 tag_prefix="$(read_field "$upstream" tag_prefix)" 477 tag_suffix="$(read_field "$upstream" tag_suffix)" 478 asset="$(read_field "$upstream" asset)" 479 [[ -n "$repo" ]] || die "${name}: upstream.json missing github" 480 [[ -n "$asset" ]] || die "${name}: upstream.json missing asset" 481 tag_prefix="${tag_prefix:-v}" 482 tag_suffix="${tag_suffix:-}" 483 484 local cur latest 485 cur="$(current_version "$default_nix")" 486 log "${name}: current=${cur}" 487 latest="$(latest_github_tag_with_suffix "$repo" "$tag_prefix" "$tag_suffix")" 488 log "${name}: latest upstream=${latest}" 489 490 if [[ "$latest" == "$cur" ]]; then 491 log "${name}: already up to date" 492 return 0 493 fi 494 495 # Semver compare on the numeric core (ignore -pre etc.) 496 local cur_core latest_core 497 cur_core="${cur%%-*}"; cur_core="${cur_core%%+*}" 498 latest_core="${latest%%-*}"; latest_core="${latest_core%%+*}" 499 if ! version_gt "$latest_core" "$cur_core" && [[ "$latest" != "$cur" ]]; then 500 # Same core version but different suffix, or non-monotonic tag: still update 501 # when the full tag string differs (handled above). If latest core is older, 502 # skip to avoid downgrades. 503 if ! version_gt "$latest_core" "$cur_core" && [[ "$latest_core" != "$cur_core" ]]; then 504 log "${name}: latest core ${latest_core} is not newer than ${cur_core}; skipping" 505 return 0 506 fi 507 fi 508 509 if [[ "$CHECK_ONLY" -eq 1 ]]; then 510 log "${name}: OUTDATED (${cur} -> ${latest})" 511 return 10 512 fi 513 514 log "${name}: updating ${cur} -> ${latest}" 515 set_field_string "$default_nix" version "$latest" 516 517 local src_url src_hash 518 src_url="https://github.com/${repo}/releases/download/${tag_prefix}${latest}/${asset}" 519 log "${name}: prefetching ${src_url}" 520 src_hash="$(sri_from_url_file "$src_url")" 521 set_fetchurl_hash "$default_nix" "$src_hash" 522 log "${name}: src hash ${src_hash}" 523 524 verify_build "$name" 525 log "${name}: updated successfully to ${latest}" 526} 527 528update_github_unstable() { 529 # Track tip of a branch for projects without release tags. 530 # version format: 0-unstable-YYYY-MM-DD 531 local name="$1" 532 local pkg_dir="$ROOT/packages/${name}" 533 local default_nix="$pkg_dir/default.nix" 534 local upstream="$pkg_dir/upstream.json" 535 local repo branch 536 repo="$(read_field "$upstream" github)" 537 branch="$(read_field "$upstream" branch)" 538 [[ -n "$repo" ]] || die "${name}: upstream.json missing github" 539 branch="${branch:-master}" 540 541 local cur_rev cur_ver latest_sha latest_date latest_ver 542 cur_rev="$(current_rev "$default_nix")" 543 cur_ver="$(current_version "$default_nix")" 544 log "${name}: current=${cur_ver} (rev ${cur_rev:0:12})" 545 546 read -r latest_sha latest_date < <(latest_github_commit "$repo" "$branch") 547 latest_ver="0-unstable-${latest_date}" 548 log "${name}: latest ${branch}=${latest_sha:0:12} (${latest_date})" 549 550 if [[ "$latest_sha" == "$cur_rev" ]]; then 551 log "${name}: already up to date" 552 return 0 553 fi 554 555 if [[ "$CHECK_ONLY" -eq 1 ]]; then 556 log "${name}: OUTDATED (${cur_rev:0:12} -> ${latest_sha:0:12})" 557 return 10 558 fi 559 560 log "${name}: updating ${cur_rev:0:12} -> ${latest_sha:0:12}" 561 set_field_string "$default_nix" version "$latest_ver" 562 set_fetch_from_github_field "$default_nix" rev "$latest_sha" 563 564 local src_url src_hash 565 src_url="https://github.com/${repo}/archive/${latest_sha}.tar.gz" 566 log "${name}: prefetching src ${src_url}" 567 src_hash="$(sri_from_url_unpack "$src_url")" 568 set_fetch_from_github_field "$default_nix" hash "$src_hash" 569 log "${name}: src hash ${src_hash}" 570 571 refresh_vendored_gems "$default_nix" "$repo" "$latest_sha" 572 573 verify_build "$name" 574 log "${name}: updated successfully to ${latest_ver} (${latest_sha:0:12})" 575} 576 577UPDATED=0 578OUTDATED=0 579 580for name in "${PACKAGES[@]}"; do 581 pkg_dir="$ROOT/packages/${name}" 582 [[ -d "$pkg_dir" ]] || die "unknown package: ${name}" 583 [[ -f "$pkg_dir/upstream.json" ]] || die "${name}: missing packages/${name}/upstream.json" 584 [[ -f "$pkg_dir/default.nix" ]] || die "${name}: missing packages/${name}/default.nix" 585 586 type="$(read_field "$pkg_dir/upstream.json" type)" 587 status=0 588 case "$type" in 589 npm-github) 590 update_npm_github "$name" || status=$? 591 ;; 592 github-unstable) 593 update_github_unstable "$name" || status=$? 594 ;; 595 github-release-binary) 596 update_github_release_binary "$name" || status=$? 597 ;; 598 *) 599 die "${name}: unsupported upstream type '${type}'" 600 ;; 601 esac 602 if [[ $status -eq 10 ]]; then 603 OUTDATED=$((OUTDATED + 1)) 604 elif [[ $status -ne 0 ]]; then 605 exit "$status" 606 else 607 # detect if files changed for this package 608 if [[ "$CHECK_ONLY" -eq 0 ]] && ! git -C "$ROOT" diff --quiet -- "packages/${name}" 2>/dev/null; then 609 UPDATED=$((UPDATED + 1)) 610 fi 611 fi 612done 613 614if [[ "$CHECK_ONLY" -eq 1 ]]; then 615 if [[ "$OUTDATED" -gt 0 ]]; then 616 log "${OUTDATED} package(s) outdated" 617 exit 1 618 fi 619 log "all packages up to date" 620 exit 0 621fi 622 623log "done (${UPDATED} package dir(s) with local changes)"