Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion scripts/setup-jetson.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,61 @@ configure_jetson_host() {
case "$jetpack_version" in
jp6)
"${SUDO[@]}" update-alternatives --set iptables /usr/sbin/iptables-legacy
"${SUDO[@]}" sed -i '/"iptables": false,/d; /"bridge": "none"/d; s/"default-runtime": "nvidia",/"default-runtime": "nvidia"/' /etc/docker/daemon.json
# Patch /etc/docker/daemon.json using Python to avoid generating invalid JSON.
# The previous sed approach stripped the trailing comma from
# "default-runtime": "nvidia", which produced malformed JSON when
# "runtimes" was the next key. See: https://github.com/NVIDIA/NemoClaw/issues/1875
"${SUDO[@]}" python3 --version >/dev/null 2>&1 \
|| error "python3 is required to patch /etc/docker/daemon.json but was not found on PATH"
"${SUDO[@]}" python3 - /etc/docker/daemon.json <<'PYEOF'
import json, os, re, sys, tempfile
path = sys.argv[1]
try:
with open(path) as f:
cfg = json.load(f)
except FileNotFoundError:
cfg = {}
except json.JSONDecodeError:
# Attempt to repair the known missing-comma pattern introduced by the
# previous sed-based approach before re-parsing. If repair fails, abort
# rather than silently overwriting the file with an empty object.
with open(path) as f:
raw = f.read()
# Insert missing comma after "default-runtime": "nvidia" when followed
# by whitespace + a quoted key (next JSON member without comma separator).
repaired = re.sub(
r'("default-runtime"\s*:\s*"nvidia")([\s\n]+")',
r'\1,\2',
raw,
)
try:
cfg = json.loads(repaired)
except json.JSONDecodeError as e:
sys.exit(f'daemon.json is malformed and could not be repaired automatically: {e}')
if not isinstance(cfg, dict):
sys.exit('daemon.json must contain a top-level JSON object')
cfg.pop('iptables', None)
cfg.pop('bridge', None)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
# Write atomically: dump to a temp file in the same directory, then replace.
# Copy permissions from the original file (or use 0644 if missing) so the
# replaced file is world-readable, matching the typical daemon.json mode.
dirname = os.path.dirname(os.path.abspath(path))
try:
orig_mode = os.stat(path).st_mode & 0o777
except FileNotFoundError:
orig_mode = 0o644
fd, tmp = tempfile.mkstemp(dir=dirname)
try:
os.chmod(tmp, orig_mode)
with os.fdopen(fd, 'w') as f:
json.dump(cfg, f, indent=4)
f.write('\n')
os.replace(tmp, path)
os.chmod(path, orig_mode)
except Exception:
os.unlink(tmp)
raise
PYEOF
;;
jp7-r38)
# JP7 R38 does not need iptables or Docker daemon.json changes.
Expand Down
141 changes: 141 additions & 0 deletions test/setup-jetson.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { execFileSync } from "node:child_process";
import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";

import { describe, expect, it } from "vitest";

const SCRIPT_PATH = path.join(import.meta.dirname, "..", "scripts", "setup-jetson.sh");

function extractDaemonJsonPatcher(): string {
const script = readFileSync(SCRIPT_PATH, "utf-8");
const match = script.match(/<<'PYEOF'\n([\s\S]*?)\nPYEOF/);
if (!match) {
throw new Error("Failed to extract inline daemon.json patcher from scripts/setup-jetson.sh");
}
return match[1];
}

function runDaemonJsonPatcher(daemonPath: string): void {
execFileSync("python3", ["-", daemonPath], {
input: extractDaemonJsonPatcher(),
encoding: "utf-8",
});
}

function getExecErrorOutput(error: unknown): string {
if (!(error instanceof Error)) {
return String(error);
}

const stderr = "stderr" in error ? error.stderr : "";
if (typeof stderr === "string") {
return stderr;
}
if (Buffer.isBuffer(stderr)) {
return stderr.toString("utf-8");
}
return error.message;
}

describe("setup-jetson daemon.json patcher", () => {
it("repairs the missing-comma regression and removes iptables and bridge keys", () => {
const tempDir = mkdtempSync(path.join(tmpdir(), "nemoclaw-jetson-patcher-"));
const daemonPath = path.join(tempDir, "daemon.json");

try {
writeFileSync(
daemonPath,
[
"{",
' "default-runtime": "nvidia"',
' "runtimes": {',
' "nvidia": {',
' "path": "nvidia-container-runtime",',
' "runtimeArgs": []',
" }",
" },",
' "iptables": false,',
' "bridge": "none"',
"}",
"",
].join("\n"),
);
chmodSync(daemonPath, 0o640);

runDaemonJsonPatcher(daemonPath);

const patched = readFileSync(daemonPath, "utf-8");
const parsed = JSON.parse(patched) as Record<string, unknown>;

expect(parsed).toEqual({
"default-runtime": "nvidia",
runtimes: {
nvidia: {
path: "nvidia-container-runtime",
runtimeArgs: [],
},
},
});
expect(patched.endsWith("\n")).toBe(true);
expect(statSync(daemonPath).mode & 0o777).toBe(0o640);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});

it("fails cleanly for unrecoverable malformed JSON without clobbering the file", () => {
const tempDir = mkdtempSync(path.join(tmpdir(), "nemoclaw-jetson-patcher-"));
const daemonPath = path.join(tempDir, "daemon.json");
const original = '{"default-runtime": "nvidia",\n';

try {
writeFileSync(daemonPath, original);

expect(() => runDaemonJsonPatcher(daemonPath)).toThrowError(
/daemon\.json is malformed and could not be repaired automatically/,
);
expect(readFileSync(daemonPath, "utf-8")).toBe(original);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});

it("rejects non-object JSON roots before mutating keys", () => {
const tempDir = mkdtempSync(path.join(tmpdir(), "nemoclaw-jetson-patcher-"));
const daemonPath = path.join(tempDir, "daemon.json");

try {
writeFileSync(daemonPath, '["not", "an", "object"]\n');

let output = "";
try {
runDaemonJsonPatcher(daemonPath);
} catch (error) {
output = getExecErrorOutput(error);
}

expect(output).toContain("daemon.json must contain a top-level JSON object");
expect(readFileSync(daemonPath, "utf-8")).toBe('["not", "an", "object"]\n');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});

it("creates a new daemon.json with 0644 permissions when the file is missing", () => {
const tempDir = mkdtempSync(path.join(tmpdir(), "nemoclaw-jetson-patcher-"));
const daemonPath = path.join(tempDir, "daemon.json");

try {
runDaemonJsonPatcher(daemonPath);

expect(readFileSync(daemonPath, "utf-8")).toBe("{}\n");
expect(statSync(daemonPath).mode & 0o777).toBe(0o644);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});
Loading