app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
main
Colby Knox
AI Flow 0.2.65: unquoted wiki symbols, current verified list, macOS desktop stand-in (#196 #192 #195)
fd77b00
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
#!/usr/bin/env bash
# kicad-cli-remote: a native KiCad 10 `kicad-cli` for a container that has none, run on a desktop
# through Adom Bridge. Implements what adom-aiflow needs (ADOM_AIFLOW_KICAD_CLI):
# kicad-cli-remote --version (this wrapper)
# kicad-cli-remote version (the desktop's kicad-cli answers)
# kicad-cli-remote pcb drc --format json --output <report.json> [--refill-zones] [--schematic-parity] <board.kicad_pcb>
# kicad-cli-remote sch erc --format json --output <report.json> [--severity-all] <design.kicad_sch>
# kicad-cli-remote sch export netlist --format kicadsexpr --output <out.net> <design.kicad_sch>
# kicad-cli-remote pcb export gerbers|drill --output <dir>/ [flags] <board.kicad_pcb>
# kicad-cli-remote pcb export pos --output <file.csv> [flags] <board.kicad_pcb>
# kicad-cli-remote pcb export step --output <file.step> [flags] <board.kicad_pcb> (native output passed through)
# The board travels with its sibling .kicad_pro / .kicad_dru so project rules are kept. The board and
# the schematic both travel with the project's sym-lib-table and fp-lib-table and every ${KIPRJMOD}
# library they name (a .pretty footprint folder with all its files), in the same folders relative to
# the project. The schematic travels with its sub-sheets and .kicad_pro. `pcb drc --schematic-parity`
# also sends the board's schematic and its sub-sheets: <board>.kicad_sch beside the board, else
# KICAD_REMOTE_SCHEMATIC (adom-aiflow sets the run's recorded schematic), sent as <board>.kicad_sch.
# Local outputs may be relative: they are resolved against the current folder before anything is pulled.
# Windows desktops run cmd through shell_execute in %TEMP%. Linux desktops (kicad-bridge's Linux build, a
# Flatpak KiCad too) run bash through run_script in ~/.adom-aiflow-kicad (home, not /tmp: a Flatpak has its own /tmp).
# macOS desktops run the same bash, through shell_execute (issue #195), in ~/.adom-aiflow-kicad.
# Env: KICAD_REMOTE_TARGET (adom-aiflow sets the run's desktop; unset: this machine's own desktop),
# KICAD_REMOTE_CLI (native kicad-cli path on it), KICAD_REMOTE_THREAD (ai-thread name for the bridge;
# default kicad-cli-remote), KICAD_REMOTE_OS (windows|linux; default: detected),
# KICAD_REMOTE_TMP (job folder parent on the desktop),
# KICAD_REMOTE_SCHEMATIC (the root .kicad_sch for `pcb drc --schematic-parity` when none is beside the board).
set -euo pipefail
T="${KICAD_REMOTE_TARGET:-}"
TH="${KICAD_REMOTE_THREAD:-kicad-cli-remote}"
# The desktop's native kicad-cli: KICAD_REMOTE_CLI, else what the KiCad bridge reports (install.kicadCli).
CLI="${KICAD_REMOTE_CLI:-}"
ROS="${KICAD_REMOTE_OS:-}"; ROS="${ROS,,}"; KSTATUS_OS=""; RTMP=""; RTMP_OURS=""
if [[ "${1:-}" == "--version" ]]; then echo "kicad-cli-remote -> ${T:-this machine} (native KiCad via Adom Bridge)"; exit 0; fi
ab() { adom-bridge ${T:+--target "$T"} "$1" "$2" --ai-thread "$TH"; }
jstdout() { python3 -c 'import sys,json
try: print(json.load(sys.stdin).get("stdout",""))
except Exception: pass'; }
# A local path made absolute (pull_file's saveTo and the existence checks must agree on it).
absp() { realpath -m -- "$1"; }
find_cli() {
if [[ -z "$CLI" ]]; then
local found
found=$(ab kicad_status '{"reason":"find the native kicad-cli for adom-aiflow"}' | python3 -c 'import sys,json
d=json.load(sys.stdin);print((d.get("install") or {}).get("kicadCli",""));print(str((d.get("platform") or {}).get("os") or ""))')
CLI="$(sed -n 1p <<<"$found")"; KSTATUS_OS="$(sed -n 2p <<<"$found")"
[[ -n "$CLI" ]] || { echo "kicad-cli-remote: no KiCad found on ${T:-this machine} (set KICAD_REMOTE_CLI)" >&2; exit 2; }
fi
find_os
}
# The desktop's OS: KICAD_REMOTE_OS, else kicad_status platform.os (kicad-bridge 1.0.56+), else
# `adom-bridge targets` os, else the shape of the kicad-cli path.
find_os() {
case "$ROS" in linux|mac|windows) return 0;; esac
local o="${ROS:-${KSTATUS_OS,,}}"
if [[ -z "$o" && -n "$T" ]]; then
o=$(adom-bridge targets --ai-thread "$TH" 2>/dev/null | python3 -c 'import sys,json
t=sys.argv[1].lower()
try: d=json.load(sys.stdin)
except Exception: d={}
for x in d.get("targets") or []:
if t in (str(x.get("name") or "").lower(), str(x.get("hostname") or "").lower()) and x.get("os"): print(str(x["os"]).lower()); break' "$T" || true)
fi
case "$o" in
*linux*) ROS=linux;;
*darwin*|*macos*|*mac\ os*|osx|mac) ROS=mac;;
*windows*|win*) ROS=windows;;
*) if [[ "$CLI" == /Applications/* ]]; then ROS=mac; elif [[ "$CLI" == /* ]]; then ROS=linux; else ROS=windows; fi;;
esac
}
# Linux and macOS desktops take bash; Windows takes cmd lines.
posix() { [[ "$ROS" != windows ]]; }
# Linux: run a bash script on the desktop (run_script; no shell-string quoting), print the bridge reply.
# macOS (issue #195): Adom Bridge's run_script there answers "No command specified" to a scriptB64, so the
# script goes through shell_execute as base64 piped into bash (still no shell-string quoting of the script).
lx_script() {
if [[ "$ROS" == mac ]]; then
ab shell_execute "$(python3 -c 'import json,sys,base64;print(json.dumps({"command":"echo "+base64.b64encode(sys.argv[1].encode()).decode()+" | base64 -d | bash","timeoutSeconds":int(sys.argv[2]),"reason":sys.argv[3]}))' "$1" "$2" "$3")"
return
fi
ab run_script "$(python3 -c 'import json,sys,base64;print(json.dumps({"interpreter":"bash","scriptB64":base64.b64encode(sys.argv[1].encode()).decode(),"timeoutSeconds":int(sys.argv[2]),"reason":sys.argv[3]}))' "$1" "$2" "$3")"
}
# Linux: one command line, every argument quoted for bash.
lx_cmd() { printf '%q ' "$@"; }
find_tmp() {
# The job folder parent: KICAD_REMOTE_TMP, else %TEMP% (Windows) or ~/.adom-aiflow-kicad (Linux).
RTMP="${KICAD_REMOTE_TMP:-}"
[[ -n "$RTMP" ]] && return 0
if posix; then
RTMP_OURS=1
RTMP=$(lx_script 'mkdir -p "$HOME/.adom-aiflow-kicad" && printf %s "$HOME/.adom-aiflow-kicad"' 30 "job folder for an adom-aiflow kicad-cli job" | jstdout | tr -d '\r\n')
[[ "$RTMP" == /* ]] || { echo "kicad-cli-remote: could not make a job folder in the home folder on ${T:-this machine}" >&2; exit 2; }
else
RTMP=$(ab shell_execute '{"command":"echo %TEMP%","reason":"temp folder for an adom-aiflow kicad-cli job"}' | jstdout | tr -d '\r\n' | tr '\\' /)
[[ -n "$RTMP" ]] || { echo "kicad-cli-remote: no temp folder on ${T:-this machine}" >&2; exit 2; }
fi
}
cleanup_remote() {
if posix; then
# only a job folder, and only inside the job parent
[[ "$1" == "$RTMP"/aiflow-* ]] && lx_script "rm -rf -- $(printf '%q' "$1")${RTMP_OURS:+; rmdir -- $(printf '%q' "$RTMP") 2>/dev/null}; true" 60 "clean up the kicad-cli job folder" >/dev/null || true
return 0
fi
ab shell_execute "$(python3 -c 'import json,sys;print(json.dumps({"command":"rmdir /s /q \""+sys.argv[1].replace("/","\\")+"\"","reason":"clean up the kicad-cli temp folder"}))' "$1")" >/dev/null || true
}
# Send local files to one remote folder: <dest> <files...>
send_to() {
local d="$1"; shift
local reply moves
reply=$(ab send_files "$(python3 -c 'import json,sys;print(json.dumps({"filePaths":sys.argv[2:],"dest":sys.argv[1],"reason":"files for a kicad-cli job for adom-aiflow (native KiCad on the desktop)"}))' "$d" "$@")")
posix || return 0
# issue #195: Adom Bridge on macOS saves into ~/Downloads whatever dest says, and its destinationPaths
# say where; move each file that landed elsewhere into the job folder
moves=$(python3 -c 'import json,os,shlex,sys
d=sys.argv[1].rstrip("/")
try: r=json.loads(sys.argv[2])
except Exception: r={}
for p in r.get("destinationPaths") or []:
p=str(p)
if os.path.dirname(p.rstrip("/")) != d: print("mv -f -- %s %s" % (shlex.quote(p), shlex.quote(d + "/" + os.path.basename(p.rstrip("/")))))' "$d" "$reply")
[[ -n "$moves" ]] || return 0
lx_script "mkdir -p $(printf '%q' "$d") && $(tr '\n' ';' <<<"$moves")" 120 "move the sent files into the kicad-cli job folder" >/dev/null
}
# The project files a kicad-cli job needs, grouped by folder relative to the job folder, one
# "<sub>\t<json list>" line per folder. Modes:
# sch <root.kicad_sch> the hierarchy, its .kicad_pro, the lib tables and their libraries
# board <board.kicad_pcb> [sch] [stage] the board, .kicad_pro, .kicad_dru, the lib tables and their
# libraries, and with a schematic its hierarchy, the root sent as
# <board>.kicad_sch (KiCad's parity check reads that name; a copy
# under that name is made in <stage> when the names differ)
# Library tables: sym-lib-table and fp-lib-table both, and every ${KIPRJMOD} URI they name, a file or a
# folder (a .pretty footprint library: all its files, in their folders).
PROJECT_FILES_PY=$(cat <<'PY'
import json, os, re, shutil, sys
mode, main = sys.argv[1], os.path.realpath(sys.argv[2])
sch = os.path.realpath(sys.argv[3]) if len(sys.argv) > 3 and sys.argv[3] else ""
stage = sys.argv[4] if len(sys.argv) > 4 else ""
groups = {}
def add(path, base, sub=None):
if not os.path.isfile(path): return
if sub is None:
rel = os.path.relpath(os.path.dirname(path), base)
sub = "" if rel == "." or rel.startswith("..") else rel.replace(os.sep, "/")
groups.setdefault(sub, set()).add(path)
def hierarchy(root):
seen, todo = [], [root]
while todo:
s = todo.pop()
if s in seen or not os.path.isfile(s): continue
seen.append(s)
text = open(s, encoding="utf-8", errors="replace").read()
for m in re.finditer(r'\(property\s+"Sheet ?[Ff]ile(?:name)?"\s+"([^"]+)"', text):
todo.append(os.path.normpath(os.path.join(os.path.dirname(s), m.group(1))))
return seen
def lib_tables(proj):
for name in ("sym-lib-table", "fp-lib-table"):
table = os.path.join(proj, name)
if not os.path.isfile(table): continue
add(table, proj)
for uri in re.findall(r'\(uri\s+"([^"]+)"\)', open(table, encoding="utf-8", errors="replace").read()):
if "${KIPRJMOD}" not in uri: continue
p = os.path.normpath(uri.replace("${KIPRJMOD}", proj))
if os.path.isfile(p): add(p, proj)
elif os.path.isdir(p):
for d, _, names in os.walk(p):
for n in sorted(names): add(os.path.join(d, n), proj)
if mode == "sch":
proj = os.path.dirname(main)
for f in hierarchy(main): add(f, proj)
add(os.path.splitext(main)[0] + ".kicad_pro", proj)
lib_tables(proj)
else:
proj = os.path.dirname(main); stem = os.path.splitext(main)[0]
add(main, proj)
for ext in (".kicad_pro", ".kicad_dru"): add(stem + ext, proj)
lib_tables(proj)
if sch:
sproj = os.path.dirname(sch)
# a board snapshot beside no lib tables: the schematic's project has them
if not any(os.path.isfile(os.path.join(proj, n)) for n in ("sym-lib-table", "fp-lib-table")): lib_tables(sproj)
want = os.path.basename(stem) + ".kicad_sch"
for f in hierarchy(sch):
if f == sch:
if os.path.basename(sch) != want:
os.makedirs(stage, exist_ok=True); f = os.path.join(stage, want); shutil.copyfile(sch, f)
add(f, sproj, "")
else: add(f, sproj)
for k in sorted(groups): print(k + "\t" + json.dumps(sorted(groups[k])))
PY
)
# Send grouped files ("<sub>\t<json list>" lines on stdin) into <dest>/<sub>.
send_groups() {
local dest="$1" line sub list d files
while IFS= read -r line; do
[[ -n "$line" ]] || continue
sub="${line%%$'\t'*}"; list="${line#*$'\t'}"
d="$dest"; [[ -n "$sub" ]] && d="$dest/$sub"
mapfile -t files < <(python3 -c 'import json,sys;[print(f) for f in json.loads(sys.argv[1])]' "$list")
[[ ${#files[@]} -gt 0 ]] && send_to "$d" "${files[@]}"
done
}
# The board, its project files, lib tables and ${KIPRJMOD} libraries into <dest>; with a schematic
# (<board> <dest> <root.kicad_sch>) its hierarchy too, the root under the board's name.
send_board() {
local board="$1" dest="$2" sch="${3:-}" stage groups
stage="$(mktemp -d)"
groups=$(python3 -c "$PROJECT_FILES_PY" board "$board" "$sch" "$stage")
send_groups "$dest" <<<"$groups"
rm -rf "$stage"
}
# The schematic hierarchy, project, lib tables and ${KIPRJMOD} libraries into <dest>, in the same
# folders relative to the project.
send_schematic() {
local groups
groups=$(python3 -c "$PROJECT_FILES_PY" sch "$1")
send_groups "$2" <<<"$groups"
}
# Run the desktop's kicad-cli: <timeout> <reason> <mkdir-or-empty> <args...>. Remote paths use forward
# slashes; Windows gets backslashes and quotes, Linux gets bash quoting. Prints the bridge reply.
RENV=() # NAME=value pairs set on the desktop for the next run_cli (issue #177: the 3rd-party model folder)
run_cli() {
local to="$1" why="$2" mk="$3"; shift 3
if posix; then
local envp=""; [[ ${#RENV[@]} -gt 0 ]] && envp="env $(lx_cmd "${RENV[@]}")"
lx_script "${mk:+mkdir -p $(printf '%q' "$mk") && }${envp}$(lx_cmd "$CLI" "$@")" "$to" "$why"
else
# every path quoted; a folder output keeps its trailing separator, doubled before the closing quote so
# Windows does not read \" as a quote
ab shell_execute "$(RENV_JSON="$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1:]))' "${RENV[@]}")" python3 - "$to" "$why" "$mk" "$CLI" "$@" <<'PY'
import json, os, sys
to, why, mk, cli, args = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5:]
def q(a):
a = a.replace("/", "\\")
if "\\" in a or " " in a or ":" in a:
return '"' + (a + "\\" if a.endswith("\\") else a) + '"'
return a
c = q(cli) + " " + " ".join(q(a) for a in args)
for kv in json.loads(os.environ.get("RENV_JSON") or "[]"):
k, _, v = kv.partition("=")
c = 'set "' + k + '=' + v.replace("/", "\\") + '" && ' + c
if mk: c = 'mkdir ' + q(mk) + ' 2>nul & ' + c
print(json.dumps({"command": c, "timeoutSeconds": int(to), "reason": why}))
PY
)"
fi
}
# The names in a remote folder, one per line.
list_remote() {
if posix; then
lx_script "cd $(printf '%q' "$1") && ls -1A" 60 "list the kicad-cli outputs" | jstdout
else
ab shell_execute "$(python3 -c 'import json,sys;print(json.dumps({"command":"dir /b \""+sys.argv[1].replace("/","\\")+"\"","reason":"list the kicad-cli outputs"}))' "$1")" | jstdout
fi | tr -d '\r' | sed '/^[[:space:]]*$/d'
}
# Pull remote files into an absolute local folder: <saveTo> <remote paths...>. Linux: a file pull_file did
# not land (a mesh-only box the relay does not hold answers target_not_found) comes back in ranged
# read_file pieces, checked against the desktop's sha256sum.
pull_to() {
local save; save="$(absp "$1")"; shift
mkdir -p "$save"
ab pull_file "$(python3 -c 'import json,sys;print(json.dumps({"filePaths":sys.argv[2:],"saveTo":sys.argv[1],"reason":"bring the kicad-cli outputs back"}))' "$save" "$@")" >/dev/null 2>&1 || true
posix || return 0
local miss=() p sums
for p in "$@"; do [[ -f "$save/$(basename "$p")" ]] || miss+=("$p"); done
[[ ${#miss[@]} -eq 0 ]] && return 0
sums=$(lx_script "{ sha256sum -- $(printf '%q ' "${miss[@]}") 2>/dev/null || shasum -a 256 -- $(printf '%q ' "${miss[@]}"); }" 60 "checksums of the kicad-cli outputs" | jstdout || true)
python3 - "$T" "$TH" "$save" "$sums" "${miss[@]}" <<'PY'
import base64, hashlib, json, os, subprocess, sys
target, thread, save, sums, paths = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5:]
want = {}
for line in sums.splitlines():
h, _, name = line.partition(" ")
if name: want[name.strip()] = h.strip()
for p in paths:
data, off = b"", 0
while True:
args = json.dumps({"path": p, "encoding": "base64", "offset": off, "length": 1048576, "reason": "bring a kicad-cli output back"})
cmd = ["adom-bridge"] + (["--target", target] if target else []) + ["read_file", args, "--ai-thread", thread]
r = subprocess.run(cmd, capture_output=True, text=True)
try: d = json.loads(r.stdout)
except Exception: d = {}
if not d.get("success"): data = None; break
data += base64.b64decode(d.get("content", ""))
if d.get("eof") or d.get("nextOffset") is None or d.get("nextOffset") == off: break
off = d["nextOffset"]
if data is None or want.get(p) != hashlib.sha256(data).hexdigest(): continue
tmp = os.path.join(save, "." + os.path.basename(p) + ".part")
with open(tmp, "wb") as f: f.write(data)
os.replace(tmp, os.path.join(save, os.path.basename(p)))
PY
}
tail_of() { python3 -c 'import sys,json
try: d=json.load(sys.stdin)
except Exception: sys.exit(0)
c=lambda t,n=800:t if len(t)<=n else t[:560]+" ... "+t[-235:]
print(c(d.get("stdout","")),c(d.get("stderr","")))' >&2 || true; }
new_job() { find_cli; find_tmp; JOB="$RTMP/aiflow-$1-$(date +%s)-$$"; }
# Bring one report back into a local file: <remote file> <local file>. Returns 1 when it did not come back.
fetch_one() {
local remote="$1" out tmp
out="$(absp "$2")"; tmp="$(mktemp -d)"
pull_to "$tmp" "$remote"
if [[ -f "$tmp/$(basename "$remote")" ]]; then
mkdir -p "$(dirname "$out")"; mv "$tmp/$(basename "$remote")" "$out"; rmdir "$tmp" 2>/dev/null || true; return 0
fi
rm -rf "$tmp"; return 1
}
if [[ "${1:-}" == "version" ]]; then
# the desktop's own kicad-cli answers (a probe; no files travel)
find_cli
v=$(run_cli 60 "kicad-cli version probe for adom-aiflow" "" version | jstdout | tr -d '\r' | sed '/^[[:space:]]*$/d')
[[ -n "$v" ]] || { echo "kicad-cli-remote: no version came back from ${T:-this machine}" >&2; exit 1; }
echo "$v"; exit 0
fi
if [[ "${1:-}" == "sch" && ( "${2:-}" == "erc" || ( "${2:-}" == "export" && "${3:-}" == "netlist" ) ) ]]; then
if [[ "$2" == "erc" ]]; then kind=erc; shift 2; else kind=netlist; shift 3; fi
if [[ " $* " == *" --help "* || " $* " == *" -h "* ]]; then
if [[ $kind == erc ]]; then
echo "Usage: kicad-cli-remote sch erc [--format json|report] --output FILE [--severity-all|--severity-error|--severity-warning] [--units mm|in|mils] INPUT_FILE"
echo " Runs the desktop's native KiCad ERC and pulls the report back (default --format json --severity-all)."
else
echo "Usage: kicad-cli-remote sch export netlist [--format kicadsexpr] --output FILE INPUT_FILE"
echo " --format kicadsexpr (default), kicadxml, orcadpcb2, ... (passed to the desktop's native KiCad)"
fi
exit 0
fi
fmt=""; out=""; sch=""; extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
--format) fmt="$2"; shift 2;;
--output|-o) out="$2"; shift 2;;
--units) extra+=("$1" "$2"); shift 2;;
--severity-all|--severity-error|--severity-warning|--severity-exclusions|--exit-code-violations) extra+=("$1"); shift;;
--*) shift;;
*) sch="$1"; shift;;
esac
done
[[ -f "$sch" && -n "$out" ]] || { echo "kicad-cli-remote: need --output and an existing schematic" >&2; exit 2; }
base="$(basename "$sch" .kicad_sch)"
new_job "$kind"
send_schematic "$sch" "$JOB"
rsch="$JOB/$base.kicad_sch"
if [[ $kind == erc ]]; then
fmt="${fmt:-json}"
[[ " ${extra[*]:-} " == *" --severity-"* ]] || extra+=(--severity-all)
rout="$JOB/$base.erc.$fmt"
res=$(run_cli 300 "schematic ERC for adom-aiflow" "" sch erc --format "$fmt" "${extra[@]}" --output "$rout" "$rsch") || true
else
fmt="${fmt:-kicadsexpr}"
rout="$JOB/$base.aiflow.net"
res=$(run_cli 180 "schematic netlist export for adom-aiflow" "" sch export netlist --format "$fmt" --output "$rout" "$rsch") || true
fi
if fetch_one "$rout" "$out"; then cleanup_remote "$JOB"; exit 0; fi
cleanup_remote "$JOB"
echo "kicad-cli-remote: no $kind report came back" >&2
echo "$res" | tail_of
exit 1
fi
if [[ "${1:-}" == "pcb" && "${2:-}" == "export" && "${3:-}" == "step" ]]; then
# The board twin's STEP: the board, its project files and every ${KIPRJMOD} model travel in their
# folders; the native output (stdout and stderr, where KiCad prints geometry warnings) is passed
# through unchanged and the native exit status is kept, so adom-aiflow step-export can classify it.
shift 3
if [[ " $* " == *" --help "* || " $* " == *" -h "* ]]; then
echo "Usage: kicad-cli-remote pcb export step [flags] --output OUT.step INPUT_FILE (flags are passed to the desktop's native KiCad)"
exit 0
fi
out=""; board=""; pass=()
while [[ $# -gt 0 ]]; do
case "$1" in
--output|-o) out="$2"; shift 2;;
--min-distance|--user-origin|--component-filter|--net-filter|--variant) pass+=("$1" "$2"); shift 2;;
--*) pass+=("$1"); shift;;
*) board="$1"; shift;;
esac
done
[[ -f "$board" && -n "$out" ]] || { echo "kicad-cli-remote: need --output and an existing board" >&2; exit 2; }
base="$(basename "$board" .kicad_pcb)"
new_job step
# the board, its project files and every ${KIPRJMOD} model, in their folders relative to the project
RENV=()
while IFS= read -r line; do
if [[ "$line" == ENV$'\t'* ]]; then
kv="${line#ENV$'\t'}"; RENV+=("${kv%%$'\t'*}=$JOB/${kv#*$'\t'}"); continue
fi
sub="${line%%$'\t'*}"; list="${line#*$'\t'}"
d="$JOB"; [[ -n "$sub" ]] && d="$JOB/$sub"
mapfile -t files < <(python3 -c 'import json,sys;[print(f) for f in json.loads(sys.argv[1])]' "$list")
send_to "$d" "${files[@]}"
done < <(python3 - "$(readlink -f "$board")" <<'PY'
import json, os, re, sys
b = sys.argv[1]; proj = os.path.dirname(b); stem = os.path.splitext(b)[0]
files = {b}
for ext in (".kicad_pro", ".kicad_dru"):
if os.path.isfile(stem + ext): files.add(stem + ext)
# ${KICAD<N>_3RD_PARTY} set here (step-export points it at the project's kicad/3rdparty) and inside the
# project: those models travel too, and the variable is set to their folder on the desktop (issue #177)
third = {k: os.path.normpath(v) for k, v in os.environ.items() if re.fullmatch(r"KICAD\d+_3RD_PARTY", k) and os.path.isdir(v)}
renv = {}
for m in re.findall(r'\(model\s+"([^"]+)"', open(b, encoding="utf-8", errors="replace").read()):
p = None
if "${KIPRJMOD}" in m:
p = os.path.normpath(m.replace("${KIPRJMOD}", proj))
for k, v in third.items():
if "${" + k + "}" in m and not os.path.relpath(v, proj).startswith(".."):
p = os.path.normpath(m.replace("${" + k + "}", v)); renv[k] = os.path.relpath(v, proj).replace(os.sep, "/")
if p:
for c in (p, os.path.splitext(p)[0] + ".step", os.path.splitext(p)[0] + ".stp"):
if os.path.isfile(c): files.add(c)
for k, rel in renv.items(): print("ENV\t" + k + "\t" + rel)
groups = {}
for f in sorted(files):
rel = os.path.relpath(os.path.dirname(f), proj)
if rel.startswith(".."): continue
groups.setdefault("" if rel == "." else rel.replace(os.sep, "/"), []).append(f)
for k, v in groups.items(): print(k + "\t" + json.dumps(v))
PY
)
rstep="$JOB/$base.aiflow.step"
res=$(run_cli 600 "board STEP export for adom-aiflow" "" pcb export step "${pass[@]}" --output "$rstep" "$JOB/$base.kicad_pcb") || true
# the native output and exit status, unchanged (Windows shell_execute and Linux run_script both answer stdout/stderr/exitCode)
echo "$res" | python3 -c 'import sys,json
try: d=json.load(sys.stdin)
except Exception: sys.exit(0)
sys.stdout.write(d.get("stdout","") or "");sys.stderr.write(d.get("stderr","") or "")' || true
rc=$(echo "$res" | python3 -c 'import sys,json
try: d=json.load(sys.stdin)
except Exception: print(1); sys.exit(0)
c=d.get("exitCode", d.get("code")); print(1 if c is None else int(c))' 2>/dev/null || echo 1)
if fetch_one "$rstep" "$out"; then cleanup_remote "$JOB"; exit "$rc"; fi
cleanup_remote "$JOB"
echo "kicad-cli-remote: the STEP did not come back" >&2
exit $(( rc == 0 ? 1 : rc ))
fi
if [[ "${1:-}" == "pcb" && "${2:-}" == "export" && "${3:-}" =~ ^(gerbers|drill|pos)$ ]]; then
kind="$3"; shift 3
if [[ " $* " == *" --help "* || " $* " == *" -h "* ]]; then
echo "Usage: kicad-cli-remote pcb export $kind [flags] --output OUT INPUT_FILE (flags are passed to the desktop's native KiCad)"
exit 0
fi
out=""; board=""; pass=()
while [[ $# -gt 0 ]]; do
case "$1" in
--output|-o) out="$2"; shift 2;;
--layers|-l|--format|--units|--side|--excellon-units|-u|--excellon-oval-format|--excellon-zeros-format|--drill-origin|--map-format|--gerber-precision|--precision|--common-layers|--cl) pass+=("$1" "$2"); shift 2;;
--*) pass+=("$1"); shift;;
*) board="$1"; shift;;
esac
done
[[ -f "$board" && -n "$out" ]] || { echo "kicad-cli-remote: need --output and an existing board" >&2; exit 2; }
base="$(basename "$board" .kicad_pcb)"
new_job export
send_board "$board" "$JOB"
rdir="$JOB/out"
rout="$rdir/"; [[ "$kind" == "pos" ]] && rout="$rdir/$(basename "$out")"
res=$(run_cli 600 "manufacturing export for adom-aiflow" "$rdir" pcb export "$kind" "${pass[@]}" --output "$rout" "$JOB/$base.kicad_pcb") || true
# the local side, absolute (#61, #72): pull_file's saveTo and the checks below must name the same folder
if [[ "$kind" == "pos" ]]; then
names=("$(basename "$out")"); local_dir="$(dirname "$(absp "$out")")"
else
local_dir="$(absp "${out%/}")"
mapfile -t names < <(list_remote "$rdir")
fi
mkdir -p "$local_dir"
code=0
if [[ ${#names[@]} -eq 0 ]]; then code=1; else
paths=(); for n in "${names[@]}"; do paths+=("$rdir/$n"); done
pull_to "$local_dir" "${paths[@]}"
for n in "${names[@]}"; do [[ -f "$local_dir/$n" ]] || { code=1; echo "kicad-cli-remote: $n did not come back to $local_dir" >&2; }; done
fi
cleanup_remote "$JOB"
if [[ $code -ne 0 ]]; then
echo "kicad-cli-remote: the $kind export did not come back" >&2
echo "$res" | tail_of
fi
exit $code
fi
[[ "${1:-}" == "pcb" && "${2:-}" == "drc" ]] || { echo "kicad-cli-remote: only 'pcb drc', 'sch erc', 'pcb export gerbers|drill|pos|step', 'sch export netlist', 'version' and '--version' are supported" >&2; exit 2; }
shift 2
if [[ " $* " == *" --help "* || " $* " == *" -h "* ]]; then
echo "Usage: kicad-cli-remote pcb drc [--format json] [--output FILE] [--refill-zones] [--schematic-parity] [--severity-all] INPUT_FILE"
echo " --refill-zones Refill zones before running DRC (passed to the desktop's native KiCad)"
echo " --schematic-parity Test the board against its schematic (<board>.kicad_sch, else KICAD_REMOTE_SCHEMATIC; sent with its sub-sheets)"
exit 0
fi
fmt=json; out=""; board=""; extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
--format) fmt="$2"; shift 2;;
--output|-o) out="$2"; shift 2;;
--refill-zones|--severity-all|--severity-error|--severity-warning|--severity-exclusions|--all-track-errors|--schematic-parity|--exit-code-violations) extra+=("$1"); shift;;
--units) extra+=("$1" "$2"); shift 2;;
--*) shift;;
*) board="$1"; shift;;
esac
done
[[ -f "$board" && -n "$out" ]] || { echo "kicad-cli-remote: need --output and an existing board" >&2; exit 2; }
base="$(basename "$board" .kicad_pcb)"
# --schematic-parity: the schematic beside the board (KiCad's own choice), else KICAD_REMOTE_SCHEMATIC
psch=""
if [[ " ${extra[*]:-} " == *" --schematic-parity "* ]]; then
if [[ -f "${board%.kicad_pcb}.kicad_sch" ]]; then psch="${board%.kicad_pcb}.kicad_sch"
elif [[ -n "${KICAD_REMOTE_SCHEMATIC:-}" && -f "$KICAD_REMOTE_SCHEMATIC" ]]; then psch="$KICAD_REMOTE_SCHEMATIC"
else echo "kicad-cli-remote: --schematic-parity but no $base.kicad_sch beside the board and no KICAD_REMOTE_SCHEMATIC: KiCad will find no schematic to compare" >&2
fi
fi
new_job drc
send_board "$board" "$JOB" "$psch"
rrep="$JOB/$base.drc.$fmt"
res=$(run_cli 240 "offline KiCad DRC for adom-aiflow" "" pcb drc "${extra[@]}" --format "$fmt" --output "$rrep" "$JOB/$base.kicad_pcb") || true
if fetch_one "$rrep" "$out"; then cleanup_remote "$JOB"; exit 0; fi
cleanup_remote "$JOB"
echo "kicad-cli-remote: no report came back" >&2
echo "$res" | tail_of
exit 1