@le-space/rootfs 0.1.4 → 0.1.6

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.
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ ALEPH_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
+ PROJECT_DIR="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
7
+ OUT_DIR="${OUT_DIR:-${ALEPH_DIR}/dist-rootfs}"
8
+ ROOTFS_CONTRACT_FILE="${ROOTFS_CONTRACT_FILE:-${ALEPH_DIR}/root-profiles/uc-go-peer.json}"
9
+ ROOTFS_BUILD_DRIVER="${ROOTFS_BUILD_DRIVER:-auto}"
10
+ ROOTFS_SIZE_MIB="${ROOTFS_SIZE_MIB:-20480}"
11
+ ROOTFS_IMAGE_SIZE="${ROOTFS_IMAGE_SIZE:-20G}"
12
+ ROOTFS_VERSION="${ROOTFS_VERSION:-}"
13
+ CHANNEL="${CHANNEL:-ALEPH-CLOUDSOLUTIONS}"
14
+ SKIP_UPLOAD="${SKIP_UPLOAD:-0}"
15
+ SKIP_BUILD="${SKIP_BUILD:-0}"
16
+ IPFS_ADD_URL="${IPFS_ADD_URL:-https://ipfs.aleph.cloud/api/v0/add}"
17
+ IPFS_GATEWAY_URL="${IPFS_GATEWAY_URL:-https://ipfs.aleph.cloud/ipfs}"
18
+ ALEPH_API_HOST="${ALEPH_API_HOST:-https://api2.aleph.im}"
19
+ ALEPH_MESSAGE_WAIT_ATTEMPTS="${ALEPH_MESSAGE_WAIT_ATTEMPTS:-60}"
20
+ ALEPH_MESSAGE_WAIT_DELAY_SECONDS="${ALEPH_MESSAGE_WAIT_DELAY_SECONDS:-5}"
21
+ ALEPH_PIN_ATTEMPTS="${ALEPH_PIN_ATTEMPTS:-4}"
22
+ ALEPH_PIN_DELAY_SECONDS="${ALEPH_PIN_DELAY_SECONDS:-10}"
23
+ IPFS_GATEWAY_WAIT_ATTEMPTS="${IPFS_GATEWAY_WAIT_ATTEMPTS:-30}"
24
+ IPFS_GATEWAY_WAIT_DELAY_SECONDS="${IPFS_GATEWAY_WAIT_DELAY_SECONDS:-10}"
25
+ ROOTFS_CID=""
26
+ ROOTFS_ITEM_HASH=""
27
+
28
+ require() {
29
+ command -v "$1" >/dev/null 2>&1 || {
30
+ echo "Missing required command: $1" >&2
31
+ exit 1
32
+ }
33
+ }
34
+
35
+ die() {
36
+ echo "$*" >&2
37
+ exit 1
38
+ }
39
+
40
+ load_rootfs_contract() {
41
+ require python3
42
+ [ -f "${ROOTFS_CONTRACT_FILE}" ] || die "Rootfs contract does not exist: ${ROOTFS_CONTRACT_FILE}"
43
+
44
+ eval "$(python3 "${SCRIPT_DIR}/read-rootfs-contract.py" "${ROOTFS_CONTRACT_FILE}")"
45
+
46
+ if [ "${ROOTFS_CONTRACT_PROFILE}" != "uc-go-peer" ]; then
47
+ die "Only the uc-go-peer rootfs profile is supported, got: ${ROOTFS_CONTRACT_PROFILE}"
48
+ fi
49
+ if [ "${ROOTFS_CONTRACT_INSTALL_MODE}" != "prebaked" ]; then
50
+ die "Only prebaked install mode is supported, got: ${ROOTFS_CONTRACT_INSTALL_MODE}"
51
+ fi
52
+ }
53
+
54
+ resolve_rootfs_version() {
55
+ if [ -n "${ROOTFS_VERSION}" ]; then
56
+ printf '%s\n' "${ROOTFS_VERSION}"
57
+ return
58
+ fi
59
+
60
+ if [ -d "${PROJECT_DIR}/.git" ]; then
61
+ local short_sha
62
+ short_sha="$(git -C "${PROJECT_DIR}" rev-parse --short HEAD)"
63
+ local build_date
64
+ build_date="$(date -u +%Y%m%d)"
65
+ printf 'uc-go-peer-git-%s-%s\n' "${build_date}" "${short_sha}"
66
+ return
67
+ fi
68
+
69
+ printf 'uc-go-peer-v0.1.0\n'
70
+ }
71
+
72
+ resolve_aleph_bin() {
73
+ if [ -n "${ALEPH_BIN:-}" ]; then
74
+ printf '%s\n' "${ALEPH_BIN}"
75
+ return
76
+ fi
77
+
78
+ if command -v aleph >/dev/null 2>&1; then
79
+ command -v aleph
80
+ return
81
+ fi
82
+
83
+ die "Missing aleph CLI. Set ALEPH_BIN=/path/to/aleph or install aleph-client."
84
+ }
85
+
86
+ build_with_host_tools() {
87
+ echo "Using host virt-customize/qemu-img toolchain."
88
+ ROOTFS_CONTRACT_FILE="${ROOTFS_CONTRACT_FILE}" \
89
+ OUT_DIR="${OUT_DIR}" \
90
+ ROOTFS_IMAGE_SIZE="${ROOTFS_IMAGE_SIZE}" \
91
+ PROJECT_DIR="${PROJECT_DIR}" \
92
+ bash "${SCRIPT_DIR}/build-rootfs-image.sh"
93
+ }
94
+
95
+ build_with_docker() {
96
+ require docker
97
+
98
+ if ! docker info >/dev/null 2>&1; then
99
+ die "Docker is installed, but the Docker daemon is not running."
100
+ fi
101
+
102
+ echo "Using Dockerized Debian/libguestfs builder."
103
+ docker build --platform linux/amd64 \
104
+ -t uc-go-peer-rootfs-builder:local \
105
+ -f "${SCRIPT_DIR}/Dockerfile.rootfs" \
106
+ "${SCRIPT_DIR}"
107
+
108
+ docker run --rm --privileged --platform linux/amd64 \
109
+ -e LIBGUESTFS_BACKEND=direct \
110
+ -e ROOTFS_CONTRACT_FILE=/workspace/universal-connectivity/go-peer/aleph/root-profiles/uc-go-peer.json \
111
+ -e OUT_DIR=/workspace/universal-connectivity/go-peer/aleph/dist-rootfs \
112
+ -e ROOTFS_IMAGE_SIZE="${ROOTFS_IMAGE_SIZE}" \
113
+ -e PROJECT_DIR=/workspace/universal-connectivity \
114
+ -v "${PROJECT_DIR}:/workspace/universal-connectivity" \
115
+ -v "${SCRIPT_DIR}:/workspace/shared-rootfs" \
116
+ -w /workspace/shared-rootfs \
117
+ uc-go-peer-rootfs-builder:local \
118
+ /bin/bash /workspace/shared-rootfs/build-rootfs-image.sh
119
+ }
120
+
121
+ sync_manifest_copy_target() {
122
+ local manifest_path="${OUT_DIR}/rootfs-manifest.json"
123
+ local copy_target="${ROOTFS_CONTRACT_MANIFEST_COPY_TARGET:-}"
124
+ local resolved_target
125
+ local target_dir
126
+ local target_ext
127
+ local versioned_target
128
+
129
+ [ -n "${copy_target}" ] || return 0
130
+ [ -f "${manifest_path}" ] || die "Manifest does not exist: ${manifest_path}"
131
+
132
+ if [[ "${copy_target}" = /* ]]; then
133
+ resolved_target="${copy_target}"
134
+ else
135
+ resolved_target="${PROJECT_DIR}/${copy_target}"
136
+ fi
137
+
138
+ target_dir="$(dirname "${resolved_target}")"
139
+ mkdir -p "${target_dir}"
140
+ cp "${manifest_path}" "${resolved_target}"
141
+
142
+ target_ext=".json"
143
+ case "${resolved_target}" in
144
+ *.json)
145
+ target_ext=".json"
146
+ ;;
147
+ esac
148
+ versioned_target="${target_dir}/${ROOTFS_VERSION}${target_ext}"
149
+ cp "${manifest_path}" "${versioned_target}"
150
+
151
+ echo "Copied rootfs manifest to ${resolved_target}"
152
+ echo "Copied versioned rootfs manifest to ${versioned_target}"
153
+ }
154
+
155
+ write_manifest() {
156
+ local rootfs_cid="${1:-}"
157
+ local rootfs_item_hash="${2:-}"
158
+ local rootfs_source_size_bytes=""
159
+
160
+ if [ -f "${OUT_DIR}/ipfs-add-response.jsonl" ]; then
161
+ rootfs_source_size_bytes="$(python3 - "${OUT_DIR}/ipfs-add-response.jsonl" <<'PY'
162
+ import json
163
+ import sys
164
+ from pathlib import Path
165
+
166
+ lines = [line for line in Path(sys.argv[1]).read_text().splitlines() if line.strip()]
167
+ if not lines:
168
+ raise SystemExit(0)
169
+
170
+ payload = json.loads(lines[-1])
171
+ size = payload.get("Size")
172
+ if isinstance(size, str) and size.isdigit():
173
+ print(size)
174
+ elif isinstance(size, int) and size > 0:
175
+ print(size)
176
+ PY
177
+ )"
178
+ fi
179
+
180
+ {
181
+ echo '{'
182
+ echo ' "profile": "uc-go-peer",'
183
+ echo " \"version\": \"${ROOTFS_VERSION}\","
184
+ echo ' "rootfsInstallStrategy": "prebaked",'
185
+ echo ' "requiresBootstrapNetwork": false,'
186
+ echo ' "bootstrapSummary": "Dependencies are preinstalled in the image.",'
187
+ if [[ "${rootfs_source_size_bytes}" =~ ^[0-9]+$ ]]; then
188
+ echo " \"rootfsSourceSizeBytes\": ${rootfs_source_size_bytes},"
189
+ fi
190
+ printf ' "requiredPortForwards": %s,\n' "${ROOTFS_CONTRACT_PORT_FORWARDS_JSON}"
191
+ if [ -n "${rootfs_cid}" ]; then
192
+ echo " \"rootfsCid\": \"${rootfs_cid}\","
193
+ fi
194
+ if [ -n "${rootfs_item_hash}" ]; then
195
+ echo " \"rootfsItemHash\": \"${rootfs_item_hash}\","
196
+ fi
197
+ echo " \"rootfsSizeMiB\": ${ROOTFS_SIZE_MIB},"
198
+ echo " \"createdAt\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
199
+ printf ' "notes": "%s"\n' "${ROOTFS_CONTRACT_MANIFEST_NOTES}"
200
+ echo '}'
201
+ } > "${OUT_DIR}/rootfs-manifest.json"
202
+
203
+ echo "Rootfs manifest written to ${OUT_DIR}/rootfs-manifest.json"
204
+ sync_manifest_copy_target
205
+ }
206
+
207
+ wait_for_aleph_message_processed() {
208
+ require python3
209
+ require curl
210
+
211
+ local item_hash="${1:?missing item hash}"
212
+ local attempts="${2:-${ALEPH_MESSAGE_WAIT_ATTEMPTS}}"
213
+ local delay_seconds="${3:-${ALEPH_MESSAGE_WAIT_DELAY_SECONDS}}"
214
+ local api_host="${4:-${ALEPH_API_HOST}}"
215
+ local response_file
216
+ response_file="$(mktemp)"
217
+
218
+ local attempt
219
+ for attempt in $(seq 1 "${attempts}"); do
220
+ if ! curl --fail --silent --show-error \
221
+ "${api_host}/api/v0/messages/${item_hash}" \
222
+ > "${response_file}"; then
223
+ rm -f "${response_file}"
224
+ die "Failed to query Aleph message status for ${item_hash}"
225
+ fi
226
+
227
+ local status
228
+ status="$(python3 - "${response_file}" <<'PY'
229
+ import json
230
+ import sys
231
+ from pathlib import Path
232
+
233
+ payload = json.loads(Path(sys.argv[1]).read_text())
234
+ status = payload.get("status")
235
+ print(status or "")
236
+ PY
237
+ )"
238
+
239
+ case "${status}" in
240
+ processed)
241
+ rm -f "${response_file}"
242
+ return 0
243
+ ;;
244
+ rejected)
245
+ local rejection_summary
246
+ rejection_summary="$(python3 - "${response_file}" <<'PY'
247
+ import json
248
+ import sys
249
+ from pathlib import Path
250
+
251
+ payload = json.loads(Path(sys.argv[1]).read_text())
252
+ error_code = payload.get("error_code")
253
+ details = payload.get("details")
254
+ first_error = details.get("errors", [None])[0] if isinstance(details, dict) else None
255
+ if error_code == 5 and isinstance(first_error, dict):
256
+ account_balance = first_error.get("account_balance")
257
+ required_balance = first_error.get("required_balance")
258
+ if account_balance is not None and required_balance is not None:
259
+ print(f"insufficient Aleph balance: account has {account_balance}, required is {required_balance}")
260
+ raise SystemExit(0)
261
+ if error_code is None:
262
+ print(json.dumps(details or {}))
263
+ else:
264
+ print(f"error {error_code}: {json.dumps(details or {})}")
265
+ PY
266
+ )"
267
+ rm -f "${response_file}"
268
+ die "Aleph STORE message ${item_hash} was rejected: ${rejection_summary}"
269
+ ;;
270
+ "")
271
+ ;;
272
+ *)
273
+ ;;
274
+ esac
275
+
276
+ if [ "${attempt}" -lt "${attempts}" ]; then
277
+ sleep "${delay_seconds}"
278
+ fi
279
+ done
280
+
281
+ rm -f "${response_file}"
282
+ die "Aleph STORE message ${item_hash} did not become processed after ${attempts} attempts."
283
+ }
284
+
285
+ wait_for_ipfs_cid_available() {
286
+ require curl
287
+
288
+ local cid="${1:?missing cid}"
289
+ local attempts="${2:-${IPFS_GATEWAY_WAIT_ATTEMPTS}}"
290
+ local delay_seconds="${3:-${IPFS_GATEWAY_WAIT_DELAY_SECONDS}}"
291
+ local gateway_base="${4:-${IPFS_GATEWAY_URL}}"
292
+ local gateway_url="${gateway_base%/}/${cid}"
293
+ local headers_file
294
+ headers_file="$(mktemp)"
295
+
296
+ local attempt
297
+ for attempt in $(seq 1 "${attempts}"); do
298
+ : > "${headers_file}"
299
+ if curl --silent --show-error --location \
300
+ --range 0-0 \
301
+ --dump-header "${headers_file}" \
302
+ --output /dev/null \
303
+ "${gateway_url}"; then
304
+ local http_status
305
+ http_status="$(python3 - "${headers_file}" <<'PY'
306
+ import sys
307
+ from pathlib import Path
308
+
309
+ status_lines = []
310
+ for line in Path(sys.argv[1]).read_text(errors="replace").splitlines():
311
+ if line.startswith("HTTP/"):
312
+ status_lines.append(line)
313
+
314
+ if not status_lines:
315
+ print("")
316
+ else:
317
+ print(status_lines[-1].split()[1])
318
+ PY
319
+ )"
320
+
321
+ case "${http_status}" in
322
+ 200|206)
323
+ rm -f "${headers_file}"
324
+ return 0
325
+ ;;
326
+ esac
327
+ fi
328
+
329
+ if [ "${attempt}" -lt "${attempts}" ]; then
330
+ echo "CID ${cid} is not retrievable from ${gateway_base} yet (attempt ${attempt}/${attempts}); retrying in ${delay_seconds}s..." >&2
331
+ sleep "${delay_seconds}"
332
+ fi
333
+ done
334
+
335
+ rm -f "${headers_file}"
336
+ die "CID ${cid} did not become retrievable from ${gateway_base} after ${attempts} attempts."
337
+ }
338
+
339
+ upload_image() {
340
+ local aleph_bin
341
+ aleph_bin="$(resolve_aleph_bin)"
342
+
343
+ require python3
344
+ require curl
345
+
346
+ local image="${OUT_DIR}/aleph-uc-go-peer.qcow2"
347
+ [ -f "${image}" ] || die "Rootfs image does not exist: ${image}"
348
+
349
+ echo "Uploading ${image} to IPFS via ${IPFS_ADD_URL}..."
350
+ : > "${OUT_DIR}/ipfs-add-response.jsonl"
351
+ if ! curl --fail --silent --show-error \
352
+ -X POST \
353
+ -F "file=@${image}" \
354
+ "${IPFS_ADD_URL}" \
355
+ > "${OUT_DIR}/ipfs-add-response.jsonl"; then
356
+ die "IPFS upload failed for ${image}"
357
+ fi
358
+
359
+ ROOTFS_CID="$(python3 - "${OUT_DIR}/ipfs-add-response.jsonl" <<'PY'
360
+ import json
361
+ import sys
362
+ from pathlib import Path
363
+
364
+ lines = [line for line in Path(sys.argv[1]).read_text().splitlines() if line.strip()]
365
+ if not lines:
366
+ raise SystemExit("No response received from the IPFS add endpoint")
367
+
368
+ payload = json.loads(lines[-1])
369
+ cid = payload.get("Hash")
370
+ if not cid:
371
+ raise SystemExit(f"IPFS add response did not include a Hash: {payload}")
372
+
373
+ print(cid)
374
+ PY
375
+ )" || die "Failed to extract CID from ${OUT_DIR}/ipfs-add-response.jsonl"
376
+
377
+ echo "Waiting for CID ${ROOTFS_CID} to become retrievable via ${IPFS_GATEWAY_URL}..."
378
+ wait_for_ipfs_cid_available "${ROOTFS_CID}"
379
+
380
+ echo "Pinning CID ${ROOTFS_CID} on Aleph Cloud..."
381
+ local attempt
382
+ local stderr_log="${OUT_DIR}/store-message.stderr.log"
383
+ local stdout_log="${OUT_DIR}/store-message.json"
384
+ local last_error_summary=""
385
+
386
+ for attempt in $(seq 1 "${ALEPH_PIN_ATTEMPTS}"); do
387
+ : > "${stdout_log}"
388
+ : > "${stderr_log}"
389
+
390
+ echo "Aleph pin attempt ${attempt}/${ALEPH_PIN_ATTEMPTS} for CID ${ROOTFS_CID}..."
391
+ if "${aleph_bin}" file pin "${ROOTFS_CID}" \
392
+ --channel "${CHANNEL}" \
393
+ > "${stdout_log}" 2> "${stderr_log}"; then
394
+ break
395
+ fi
396
+
397
+ last_error_summary="$(python3 - "${stderr_log}" <<'PY'
398
+ import sys
399
+ from pathlib import Path
400
+
401
+ text = Path(sys.argv[1]).read_text(errors="replace").strip()
402
+ print(text or "Aleph pin failed without stderr output")
403
+ PY
404
+ )"
405
+
406
+ echo "Aleph pin attempt ${attempt}/${ALEPH_PIN_ATTEMPTS} failed for CID ${ROOTFS_CID}." >&2
407
+ if [[ -n "${last_error_summary}" ]]; then
408
+ echo "${last_error_summary}" >&2
409
+ fi
410
+
411
+ if [ "${attempt}" -lt "${ALEPH_PIN_ATTEMPTS}" ]; then
412
+ echo "Retrying Aleph pin in ${ALEPH_PIN_DELAY_SECONDS}s..." >&2
413
+ sleep "${ALEPH_PIN_DELAY_SECONDS}"
414
+ continue
415
+ fi
416
+
417
+ die "Aleph pin failed for CID ${ROOTFS_CID} after ${ALEPH_PIN_ATTEMPTS} attempts"
418
+ done
419
+
420
+ if [ ! -s "${stdout_log}" ]; then
421
+ if [ -n "${last_error_summary}" ]; then
422
+ echo "${last_error_summary}" >&2
423
+ fi
424
+ die "Aleph pin returned an empty response for CID ${ROOTFS_CID}"
425
+ fi
426
+
427
+ ROOTFS_ITEM_HASH="$(python3 - "${stdout_log}" <<'PY'
428
+ import json
429
+ import sys
430
+ from pathlib import Path
431
+
432
+ content = Path(sys.argv[1]).read_text().strip()
433
+ if not content:
434
+ raise SystemExit("Aleph pin returned an empty response")
435
+
436
+ payload = json.loads(content)
437
+ print(payload["item_hash"])
438
+ PY
439
+ )" || die "Failed to extract Aleph item hash from ${OUT_DIR}/store-message.json"
440
+
441
+ wait_for_aleph_message_processed "${ROOTFS_ITEM_HASH}"
442
+
443
+ echo "Published rootfs CID: ${ROOTFS_CID}"
444
+ echo "Published Aleph item hash: ${ROOTFS_ITEM_HASH}"
445
+ }
446
+
447
+ mkdir -p "${OUT_DIR}"
448
+ load_rootfs_contract
449
+ ROOTFS_VERSION="$(resolve_rootfs_version)"
450
+
451
+ echo "Building rootfs profile: uc-go-peer"
452
+ echo "Using install mode: prebaked"
453
+
454
+ if [ "${SKIP_BUILD}" != "1" ]; then
455
+ case "${ROOTFS_BUILD_DRIVER}" in
456
+ host)
457
+ if command -v virt-customize >/dev/null 2>&1; then
458
+ build_with_host_tools
459
+ else
460
+ die "ROOTFS_BUILD_DRIVER=host requested, but virt-customize is not available."
461
+ fi
462
+ ;;
463
+ docker)
464
+ build_with_docker
465
+ ;;
466
+ auto)
467
+ if [ "${GITHUB_ACTIONS:-}" = "true" ] && command -v docker >/dev/null 2>&1; then
468
+ build_with_docker
469
+ elif command -v virt-customize >/dev/null 2>&1; then
470
+ build_with_host_tools
471
+ else
472
+ build_with_docker
473
+ fi
474
+ ;;
475
+ *)
476
+ die "Unsupported ROOTFS_BUILD_DRIVER: ${ROOTFS_BUILD_DRIVER}"
477
+ ;;
478
+ esac
479
+ else
480
+ [ -f "${OUT_DIR}/aleph-uc-go-peer.qcow2" ] || die "SKIP_BUILD=1 requested, but image is missing."
481
+ fi
482
+
483
+ if [ "${SKIP_UPLOAD}" = "1" ]; then
484
+ write_manifest
485
+ echo "SKIP_UPLOAD=1 set; image ready at ${OUT_DIR}/aleph-uc-go-peer.qcow2"
486
+ exit 0
487
+ fi
488
+
489
+ upload_image
490
+ write_manifest "${ROOTFS_CID}" "${ROOTFS_ITEM_HASH}"
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import shlex
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def shell_assign(name: str, value: str) -> str:
13
+ return f"{name}={shlex.quote(value)}"
14
+
15
+
16
+ def main() -> int:
17
+ parser = argparse.ArgumentParser(description="Read a relay rootfs contract and emit shell exports.")
18
+ parser.add_argument("contract", type=Path, help="Path to the rootfs contract JSON file")
19
+ args = parser.parse_args()
20
+
21
+ try:
22
+ payload = json.loads(args.contract.read_text())
23
+ except FileNotFoundError:
24
+ print(f"Missing rootfs contract: {args.contract}", file=sys.stderr)
25
+ return 1
26
+ except json.JSONDecodeError as exc:
27
+ print(f"Invalid rootfs contract JSON in {args.contract}: {exc}", file=sys.stderr)
28
+ return 1
29
+
30
+ rootfs = payload.get("rootfs") or {}
31
+ services = payload.get("services") or {}
32
+ source = payload.get("source") or {}
33
+ manifest = payload.get("manifest") or {}
34
+ ports = payload.get("ports") or []
35
+
36
+ profile = rootfs.get("profile")
37
+ install_mode = rootfs.get("installMode")
38
+
39
+ if not isinstance(profile, str) or not profile.strip():
40
+ print("Rootfs contract is missing rootfs.profile", file=sys.stderr)
41
+ return 1
42
+ if not isinstance(install_mode, str) or not install_mode.strip():
43
+ print("Rootfs contract is missing rootfs.installMode", file=sys.stderr)
44
+ return 1
45
+ if not isinstance(ports, list):
46
+ print("Rootfs contract field ports must be a list", file=sys.stderr)
47
+ return 1
48
+
49
+ lines = [
50
+ shell_assign("ROOTFS_CONTRACT_PATH", str(args.contract.resolve())),
51
+ shell_assign("ROOTFS_CONTRACT_ID", str(payload.get("id", ""))),
52
+ shell_assign("ROOTFS_CONTRACT_PROFILE", profile.strip()),
53
+ shell_assign("ROOTFS_CONTRACT_INSTALL_MODE", install_mode.strip()),
54
+ shell_assign("ROOTFS_CONTRACT_SOURCE_SUBDIRECTORY", str(source.get("subdirectory", ""))),
55
+ shell_assign("ROOTFS_CONTRACT_INSTALL_DIR", str(rootfs.get("installDir", ""))),
56
+ shell_assign("ROOTFS_CONTRACT_BINARY_PATH", str(rootfs.get("binaryPath", "/usr/local/bin/universal-chat-go"))),
57
+ shell_assign("ROOTFS_CONTRACT_DATA_DIR", str(rootfs.get("dataDir", ""))),
58
+ shell_assign("ROOTFS_CONTRACT_ENV_FILE", str(rootfs.get("envFile", ""))),
59
+ shell_assign("ROOTFS_CONTRACT_MAIN_SERVICE", str(services.get("main", ""))),
60
+ shell_assign("ROOTFS_CONTRACT_BOOTSTRAP_SERVICE", str(services.get("bootstrap", ""))),
61
+ shell_assign("ROOTFS_CONTRACT_AUTOTLS_SERVICE", str(services.get("autotlsRefresh", ""))),
62
+ shell_assign("ROOTFS_CONTRACT_MANIFEST_COPY_TARGET", str(manifest.get("copyTarget", ""))),
63
+ shell_assign("ROOTFS_CONTRACT_MANIFEST_NOTES", str(manifest.get("notes", ""))),
64
+ shell_assign("ROOTFS_CONTRACT_PORT_FORWARDS_JSON", json.dumps(ports, separators=(",", ":"))),
65
+ ]
66
+
67
+ print("\n".join(lines))
68
+ return 0
69
+
70
+
71
+ if __name__ == "__main__":
72
+ raise SystemExit(main())