master
John Lauer Publish 1.1.1 552a6b7 18d ago
#!/usr/bin/env bash
# kicad-cli for an Adom container: there is NO local KiCad here, and none should ever be installed.
#   1. service-kicad (the shared KiCad 10 service) when it is up and the command maps onto it;
#   2. otherwise the native kicad-cli on the user's own desktop (Windows or Mac) through Adom Bridge:
#      input files are sent over, paths are rewritten, outputs (-o/--output) are pulled back.
# Env: KICAD_CLI_TARGET (default: the bridge's primary machine = the user's own), KICAD_CLI_THREAD,
#      KICAD_CLI_PREFER=desktop to skip the service.
# Shipped by adom/service-kicad. Never `apt-get install kicad` in a container unless the user asks for a local install.
set -uo pipefail
TH="${KICAD_CLI_THREAD:-kicad-cli shim}"
say() { echo "kicad-cli (shim): $*" >&2; }
jq_py() { python3 -c "$1" "${@:2}"; }

if [[ $# -eq 0 || "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
  cat <<'EOF'
kicad-cli (Adom shim): no local KiCad in this container. Commands run on
  1. service-kicad (KiCad 10) when it is up and the command is a plain pcb drc / sch erc / pcb export step|gerbers|svg;
  2. else the native kicad-cli on your own desktop through Adom Bridge (KiCad 10 there).
Use it exactly like kicad-cli, e.g.  kicad-cli pcb drc --format json --output drc.json board.kicad_pcb
NEVER install KiCad in the container (apt kicad): use this, or `service-kicad` directly.
EOF
  exit 0
fi

# ---- 1. the service, for the commands it implements with no extra flags
try_service() {
  [[ $# -ge 2 ]] || return 1
  [[ "${KICAD_CLI_PREFER:-}" == "desktop" ]] && return 1
  command -v service-kicad >/dev/null || return 1
  local sub="$1 $2" fmt="" out="" file="" a
  shift 2
  case "$sub" in "pcb drc"|"sch erc"|"pcb export") ;; *) return 1;; esac
  local kind=""
  if [[ "$sub" == "pcb export" ]]; then kind="$1"; shift; case "$kind" in step|gerbers|svg) ;; *) return 1;; esac; fi
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --format) fmt="$2"; shift 2;;
      --output|-o) out="$2"; shift 2;;
      -*) return 1;;                         # a flag the service does not take: use the desktop
      *) file="$1"; shift;;
    esac
  done
  [[ -f "$file" ]] || return 1
  service-kicad health >/dev/null 2>&1 || { say "service-kicad is down; using the desktop's KiCad"; return 1; }
  local cmd=(service-kicad ${sub})
  [[ -n "$kind" ]] && cmd+=("$kind")
  [[ -n "$fmt" && -z "$kind" ]] && cmd+=(--format "$fmt")
  [[ -n "$out" ]] && cmd+=(--out "$out")
  cmd+=("$file")
  say "service-kicad: ${cmd[*]:1}"
  "${cmd[@]}"; exit $?
}
try_service "$@"

# ---- 2. the user's desktop
T="${KICAD_CLI_TARGET:-$(adom-bridge targets 2>/dev/null | jq_py 'import sys,json;print(json.load(sys.stdin).get("primaryMachine",""))')}"
[[ -n "$T" ]] || { say "no service-kicad and no desktop connected through Adom Bridge. Do NOT install KiCad here: ask the user to connect their desktop, or report service-kicad down."; exit 2; }
AB=(adom-bridge --target "$T" --ai-thread "$TH")
CLI=$("${AB[@]}" kicad_status '{"reason":"find the native kicad-cli for the container kicad-cli shim"}' 2>/dev/null | jq_py 'import sys,json;d=json.load(sys.stdin);print((d.get("install") or {}).get("kicadCli",""))')
[[ -n "$CLI" ]] || { say "no KiCad found on $T. Do NOT install KiCad in the container; ask the user."; exit 2; }
WIN=0; [[ "$CLI" == *.exe || "$CLI" == [A-Za-z]:* ]] && WIN=1
if [[ $WIN == 1 ]]; then
  RTMP=$("${AB[@]}" shell_execute '{"command":"echo %TEMP%","reason":"temp folder for a kicad-cli job"}' | jq_py 'import sys,json;print(json.load(sys.stdin).get("stdout","").strip().replace(chr(92),"/"))')
else
  RTMP=$("${AB[@]}" shell_execute '{"command":"echo ${TMPDIR:-/tmp}","reason":"temp folder for a kicad-cli job"}' | jq_py 'import sys,json;print(json.load(sys.stdin).get("stdout","").strip().rstrip("/"))')
fi
job="kicad-cli-$(date +%s)-$$"; dest="$RTMP/$job"

# inputs: every existing local file argument, plus a board's / schematic's project siblings
declare -A MAP; files=(); args=(); outs=(); prev=""
for a in "$@"; do
  if [[ "$prev" == "-o" || "$prev" == "--output" ]]; then
    outs+=("$a"); args+=("$dest/out/$(basename "$a")"); prev="$a"; continue
  fi
  if [[ -f "$a" ]]; then
    f=$(readlink -f "$a"); files+=("$f"); args+=("$dest/$(basename "$f")")
    stem="${f%.*}"
    for ext in kicad_pro kicad_dru kicad_pcb kicad_sch kicad_prl; do [[ -f "$stem.$ext" && "$stem.$ext" != "$f" ]] && files+=("$stem.$ext"); done
  else
    args+=("$a")
  fi
  prev="$a"
done
if [[ ${#files[@]} -gt 0 ]]; then
  "${AB[@]}" send_files "$(jq_py 'import json,sys;print(json.dumps({"filePaths":sorted(set(sys.argv[2:])),"dest":sys.argv[1],"reason":"inputs for a kicad-cli job (no KiCad in the container)"}))' "$dest" "${files[@]}")" >/dev/null
fi
cmd=$(jq_py '
import json,sys
win=sys.argv[1]=="1"; cli=sys.argv[2]; dest=sys.argv[3]; a=sys.argv[4:]
q=lambda s: "\""+s+"\"" if (" " in s or "/" in s or "\\" in s) else s
fix=(lambda s: s.replace("/","\\")) if win else (lambda s: s)
mk=("mkdir \""+fix(dest+"/out")+"\" 2>nul & ") if win else ("mkdir -p \""+dest+"/out\" && ")
print(json.dumps({"command": mk+q(fix(cli))+" "+" ".join(q(fix(x)) if x.startswith(dest) else q(x) for x in a),"timeoutSeconds":600,"reason":"kicad-cli on the desktop (no KiCad in the container)"}))' "$WIN" "$CLI" "$dest" "${args[@]}")
say "running on $T: native KiCad"
res=$("${AB[@]}" shell_execute "$cmd")
echo "$res" | jq_py 'import sys,json;d=json.load(sys.stdin);o=d.get("stdout","");e=d.get("stderr","");sys.stdout.write(o);sys.stderr.write(e)'
code=$(echo "$res" | jq_py 'import sys,json;d=json.load(sys.stdin);print(d.get("exitCode",d.get("exit_code",0)) or 0)')
for o in "${outs[@]}"; do
  tmp=$(mktemp -d)
  "${AB[@]}" pull_file "$(jq_py 'import json,sys;print(json.dumps({"filePaths":[sys.argv[1]],"saveTo":sys.argv[2],"reason":"bring a kicad-cli output back"}))' "$dest/out/$(basename "$o")" "$tmp")" >/dev/null
  if [[ -e "$tmp/$(basename "$o")" ]]; then mkdir -p "$(dirname "$o")"; rm -rf "$o"; mv "$tmp/$(basename "$o")" "$o"; else say "output $o did not come back"; code=1; fi
  rm -rf "$tmp"
done
if [[ $WIN == 1 ]]; then rm_cmd="rmdir /s /q \"${dest//\//\\}\""; else rm_cmd="rm -rf \"$dest\""; fi
"${AB[@]}" shell_execute "$(jq_py 'import json,sys;print(json.dumps({"command":sys.argv[1],"reason":"clean up the kicad-cli job folder"}))' "$rm_cmd")" >/dev/null 2>&1 || true
exit "$code"