#!/usr/bin/env bash # # Regenerates the gRPC C# files from the Communication project's .proto files. # # Background: protoc (linux/arm64) segfaults inside our Docker build container # (Grpc.Tools). As a workaround the generated C# is checked into # src/ZB.MOM.WW.ScadaBridge.Communication/ — SiteStreamGrpc/ for sitestream.proto, # CentralControlGrpc/ for central_control.proto, and SiteCommandGrpc/ for # site_command.proto — and the Protobuf ItemGroup in the .csproj is commented out, # so Docker just compiles the checked-in files. # # Run this script ON YOUR DEV MACHINE whenever a .proto changes: # # docker/regen-proto.sh [sitestream|centralcontrol|sitecommand|all] (default: all) # # 1. Injects a Protobuf ItemGroup for the selected proto(s) so Grpc.Tools runs. # 2. Deletes the stale checked-in C# so a failed regen is obvious. # 3. dotnet build (regen writes fresh files to obj/). # 4. Copies the regenerated files back into the source tree. # 5. Restores the original csproj so no active Protobuf item is left behind. # # Only the SELECTED protos get a Protobuf item. Enabling one whose generated C# # is still checked in would define every generated type twice, which is why the # per-proto selection exists. central_control.proto imports sitestream.proto, # but protoc resolves that from the project-relative path — the import needs no # Protobuf item of its own. # # Once we move to a Dockerfile base image that ships a working linux/arm64 # protoc, this script can be retired and Docker can regen the protos on every # build like every other normal .NET project. set -euo pipefail TARGET="${1:-all}" case "$TARGET" in sitestream|centralcontrol|sitecommand|all) ;; *) echo "usage: $0 [sitestream|centralcontrol|sitecommand|all]" >&2; exit 2 ;; esac SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" COMM_DIR="$REPO_ROOT/src/ZB.MOM.WW.ScadaBridge.Communication" CSPROJ="$COMM_DIR/ZB.MOM.WW.ScadaBridge.Communication.csproj" GEN="$COMM_DIR/obj/Debug/net10.0/Protos" echo "=== Regenerating gRPC files ($TARGET) ===" if [[ ! -f "$CSPROJ" ]]; then echo "ERROR: csproj not found at $CSPROJ" >&2 exit 1 fi # Backup so we can always restore the comment state, however this script ends. # Leaving the csproj with an active Protobuf item is the one outcome that breaks # Docker, so every exit path restores this copy — success, a failed `dotnet # build` (ERR), or Ctrl-C (INT/TERM) mid-build. An EXIT trap covers all of # those (bash runs it on any shell termination, signals included), and the # guard on $BACKUP's existence makes it idempotent: the normal success path # below restores explicitly for prompt feedback, so by the time the trap fires # on a clean exit $BACKUP is already gone and it's a no-op instead of a second # restore/rm. BACKUP="$(mktemp)" cp "$CSPROJ" "$BACKUP" restore_csproj() { if [[ -f "$BACKUP" ]]; then cp "$BACKUP" "$CSPROJ" rm -f "$BACKUP" echo "Restored csproj from backup." fi } trap restore_csproj EXIT # 1. Inject an ItemGroup holding just the selected protos, immediately before # the closing . The documented commented-out block is left alone. python3 - "$CSPROJ" "$TARGET" <<'PY' import pathlib, sys csproj, target = pathlib.Path(sys.argv[1]), sys.argv[2] protos = [] if target in ("sitestream", "all"): protos.append("sitestream.proto") if target in ("centralcontrol", "all"): protos.append("central_control.proto") if target in ("sitecommand", "all"): protos.append("site_command.proto") items = "\n".join( f' ' for p in protos) block = f" \n{items}\n \n\n" src = csproj.read_text() if "" not in src: raise SystemExit("Couldn't find to inject the Protobuf ItemGroup before.") csproj.write_text(src.replace("", block, 1)) PY # 2. Delete the stale files so any failure to regen is obvious. if [[ "$TARGET" == "sitestream" || "$TARGET" == "all" ]]; then rm -f "$COMM_DIR/SiteStreamGrpc/Sitestream.cs" "$COMM_DIR/SiteStreamGrpc/SitestreamGrpc.cs" fi if [[ "$TARGET" == "centralcontrol" || "$TARGET" == "all" ]]; then rm -f "$COMM_DIR/CentralControlGrpc/CentralControl.cs" \ "$COMM_DIR/CentralControlGrpc/CentralControlGrpc.cs" fi if [[ "$TARGET" == "sitecommand" || "$TARGET" == "all" ]]; then rm -f "$COMM_DIR/SiteCommandGrpc/SiteCommand.cs" \ "$COMM_DIR/SiteCommandGrpc/SiteCommandGrpc.cs" fi # 3. Regenerate by building. echo "Building Communication project (regen)..." dotnet build "$CSPROJ" --nologo -v minimal | tail -5 # 4. Copy generated files back into the source tree. if [[ "$TARGET" == "sitestream" || "$TARGET" == "all" ]]; then mkdir -p "$COMM_DIR/SiteStreamGrpc" cp "$GEN/Sitestream.cs" "$GEN/SitestreamGrpc.cs" "$COMM_DIR/SiteStreamGrpc/" echo "Copied regenerated files to SiteStreamGrpc/" fi if [[ "$TARGET" == "centralcontrol" || "$TARGET" == "all" ]]; then mkdir -p "$COMM_DIR/CentralControlGrpc" cp "$GEN/CentralControl.cs" "$GEN/CentralControlGrpc.cs" "$COMM_DIR/CentralControlGrpc/" echo "Copied regenerated files to CentralControlGrpc/" fi if [[ "$TARGET" == "sitecommand" || "$TARGET" == "all" ]]; then mkdir -p "$COMM_DIR/SiteCommandGrpc" cp "$GEN/SiteCommand.cs" "$GEN/SiteCommandGrpc.cs" "$COMM_DIR/SiteCommandGrpc/" echo "Copied regenerated files to SiteCommandGrpc/" fi # 5. Restore the backed-up csproj — i.e. drop the injected ItemGroup — so Docker # builds keep working. (Also covered by the EXIT trap above; calling it here # too just gives the success path its "Restored..." feedback before "Done".) restore_csproj echo "" echo "Done. Review and commit:" echo " git diff src/ZB.MOM.WW.ScadaBridge.Communication/Protos/" echo " git diff src/ZB.MOM.WW.ScadaBridge.Communication/SiteStreamGrpc/" echo " git diff src/ZB.MOM.WW.ScadaBridge.Communication/CentralControlGrpc/" echo " git diff src/ZB.MOM.WW.ScadaBridge.Communication/SiteCommandGrpc/" echo " git diff -- src/ZB.MOM.WW.ScadaBridge.Communication/*.csproj # must be EMPTY"