From e85aaa2f4b9cb4771d7289de31bef53ec219fc40 Mon Sep 17 00:00:00 2001 From: chrisfu Date: Sun, 14 Dec 2025 23:14:33 -0800 Subject: [PATCH] install.py drag and drop deploy. mv gcp to deploy dir. add init-port-fowards.sh and port-mappings.properties; now configured by Prole.app --- cloudnative-pg-prole-dB.yaml | 5 + conf/port-mappings.properties | 54 ++ {gcp => deploy/gcp}/terraform/README.md | 0 {gcp => deploy/gcp}/terraform/autokey.tf | 0 {gcp => deploy/gcp}/terraform/backends.tf | 0 .../gcp}/terraform/cloud-setup.auto.tfvars | 0 {gcp => deploy/gcp}/terraform/data.tf | 0 {gcp => deploy/gcp}/terraform/folders.tf | 0 {gcp => deploy/gcp}/terraform/groups.tf | 0 {gcp => deploy/gcp}/terraform/iam.tf | 0 {gcp => deploy/gcp}/terraform/log-export.tf | 0 {gcp => deploy/gcp}/terraform/monitoring.tf | 0 {gcp => deploy/gcp}/terraform/network.tf | 0 {gcp => deploy/gcp}/terraform/org-policy.tf | 0 {gcp => deploy/gcp}/terraform/projects.tf | 0 {gcp => deploy/gcp}/terraform/providers.tf | 0 .../gcp}/terraform/service-projects.tf | 0 {gcp => deploy/gcp}/terraform/variables.tf | 0 {gcp => deploy/gcp}/terraform/versions.tf | 0 {gcp => deploy/gcp}/terraform/vpn.tf | 0 install.py | 559 ++++++++++++++---- installer/build.py | 4 +- installer/screen.py | 15 +- prole-app/Sources/AppDelegate.swift | 37 +- prole-app/Sources/MainWindowController.swift | 44 +- prole-app/Sources/OverlayWindow.swift | 9 +- prole-app/Sources/StatusView.swift | 22 +- prole-app/build.sh | 67 ++- prole-db/bulk_insert.sql | 2 +- {mssql => prole-mssql-db}/Dockerfile | 0 {mssql => prole-mssql-db}/db-service.yaml | 0 .../prole-mssql-db-deployment.yaml | 0 .../prole-mssql-db-iscsi-data001.yaml | 0 .../prole-mssql-db-nfs-data001.yaml | 0 .../prole-mssql-db-nfs-deployment.yaml | 0 35 files changed, 619 insertions(+), 199 deletions(-) create mode 100644 conf/port-mappings.properties rename {gcp => deploy/gcp}/terraform/README.md (100%) rename {gcp => deploy/gcp}/terraform/autokey.tf (100%) rename {gcp => deploy/gcp}/terraform/backends.tf (100%) rename {gcp => deploy/gcp}/terraform/cloud-setup.auto.tfvars (100%) rename {gcp => deploy/gcp}/terraform/data.tf (100%) rename {gcp => deploy/gcp}/terraform/folders.tf (100%) rename {gcp => deploy/gcp}/terraform/groups.tf (100%) rename {gcp => deploy/gcp}/terraform/iam.tf (100%) rename {gcp => deploy/gcp}/terraform/log-export.tf (100%) rename {gcp => deploy/gcp}/terraform/monitoring.tf (100%) rename {gcp => deploy/gcp}/terraform/network.tf (100%) rename {gcp => deploy/gcp}/terraform/org-policy.tf (100%) rename {gcp => deploy/gcp}/terraform/projects.tf (100%) rename {gcp => deploy/gcp}/terraform/providers.tf (100%) rename {gcp => deploy/gcp}/terraform/service-projects.tf (100%) rename {gcp => deploy/gcp}/terraform/variables.tf (100%) rename {gcp => deploy/gcp}/terraform/versions.tf (100%) rename {gcp => deploy/gcp}/terraform/vpn.tf (100%) rename {mssql => prole-mssql-db}/Dockerfile (100%) rename {mssql => prole-mssql-db}/db-service.yaml (100%) rename {mssql => prole-mssql-db}/prole-mssql-db-deployment.yaml (100%) rename {mssql => prole-mssql-db}/prole-mssql-db-iscsi-data001.yaml (100%) rename {mssql => prole-mssql-db}/prole-mssql-db-nfs-data001.yaml (100%) rename {mssql => prole-mssql-db}/prole-mssql-db-nfs-deployment.yaml (100%) diff --git a/cloudnative-pg-prole-dB.yaml b/cloudnative-pg-prole-dB.yaml index c27541c..a2707c8 100644 --- a/cloudnative-pg-prole-dB.yaml +++ b/cloudnative-pg-prole-dB.yaml @@ -31,6 +31,8 @@ spec: - CREATE EXTENSION postgis_tiger_geocoder; - CREATE EXTENSION vector; - CREATE EXTENSION tds_fdw; + # pg_tde requires the extension to be present in the image. Ensure prole-db image contains pg_tde before enabling. + - CREATE EXTENSION IF NOT EXISTS pg_tde; enableSuperuserAccess: true @@ -62,5 +64,8 @@ spec: test-label: "true" annotations: test-annotation: "true" + prometheus.io/scrape: "true" + prometheus.io/port: "9187" + prometheus.io/path: "/metrics" spec: type: LoadBalancer diff --git a/conf/port-mappings.properties b/conf/port-mappings.properties new file mode 100644 index 0000000..8fe09a0 --- /dev/null +++ b/conf/port-mappings.properties @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gcp/terraform/README.md b/deploy/gcp/terraform/README.md similarity index 100% rename from gcp/terraform/README.md rename to deploy/gcp/terraform/README.md diff --git a/gcp/terraform/autokey.tf b/deploy/gcp/terraform/autokey.tf similarity index 100% rename from gcp/terraform/autokey.tf rename to deploy/gcp/terraform/autokey.tf diff --git a/gcp/terraform/backends.tf b/deploy/gcp/terraform/backends.tf similarity index 100% rename from gcp/terraform/backends.tf rename to deploy/gcp/terraform/backends.tf diff --git a/gcp/terraform/cloud-setup.auto.tfvars b/deploy/gcp/terraform/cloud-setup.auto.tfvars similarity index 100% rename from gcp/terraform/cloud-setup.auto.tfvars rename to deploy/gcp/terraform/cloud-setup.auto.tfvars diff --git a/gcp/terraform/data.tf b/deploy/gcp/terraform/data.tf similarity index 100% rename from gcp/terraform/data.tf rename to deploy/gcp/terraform/data.tf diff --git a/gcp/terraform/folders.tf b/deploy/gcp/terraform/folders.tf similarity index 100% rename from gcp/terraform/folders.tf rename to deploy/gcp/terraform/folders.tf diff --git a/gcp/terraform/groups.tf b/deploy/gcp/terraform/groups.tf similarity index 100% rename from gcp/terraform/groups.tf rename to deploy/gcp/terraform/groups.tf diff --git a/gcp/terraform/iam.tf b/deploy/gcp/terraform/iam.tf similarity index 100% rename from gcp/terraform/iam.tf rename to deploy/gcp/terraform/iam.tf diff --git a/gcp/terraform/log-export.tf b/deploy/gcp/terraform/log-export.tf similarity index 100% rename from gcp/terraform/log-export.tf rename to deploy/gcp/terraform/log-export.tf diff --git a/gcp/terraform/monitoring.tf b/deploy/gcp/terraform/monitoring.tf similarity index 100% rename from gcp/terraform/monitoring.tf rename to deploy/gcp/terraform/monitoring.tf diff --git a/gcp/terraform/network.tf b/deploy/gcp/terraform/network.tf similarity index 100% rename from gcp/terraform/network.tf rename to deploy/gcp/terraform/network.tf diff --git a/gcp/terraform/org-policy.tf b/deploy/gcp/terraform/org-policy.tf similarity index 100% rename from gcp/terraform/org-policy.tf rename to deploy/gcp/terraform/org-policy.tf diff --git a/gcp/terraform/projects.tf b/deploy/gcp/terraform/projects.tf similarity index 100% rename from gcp/terraform/projects.tf rename to deploy/gcp/terraform/projects.tf diff --git a/gcp/terraform/providers.tf b/deploy/gcp/terraform/providers.tf similarity index 100% rename from gcp/terraform/providers.tf rename to deploy/gcp/terraform/providers.tf diff --git a/gcp/terraform/service-projects.tf b/deploy/gcp/terraform/service-projects.tf similarity index 100% rename from gcp/terraform/service-projects.tf rename to deploy/gcp/terraform/service-projects.tf diff --git a/gcp/terraform/variables.tf b/deploy/gcp/terraform/variables.tf similarity index 100% rename from gcp/terraform/variables.tf rename to deploy/gcp/terraform/variables.tf diff --git a/gcp/terraform/versions.tf b/deploy/gcp/terraform/versions.tf similarity index 100% rename from gcp/terraform/versions.tf rename to deploy/gcp/terraform/versions.tf diff --git a/gcp/terraform/vpn.tf b/deploy/gcp/terraform/vpn.tf similarity index 100% rename from gcp/terraform/vpn.tf rename to deploy/gcp/terraform/vpn.tf diff --git a/install.py b/install.py index 265826d..b06d0d0 100755 --- a/install.py +++ b/install.py @@ -72,10 +72,12 @@ class ProleInstaller: # Style configuration self.style = ttk.Style() - # Force a light, stable theme to avoid dark/black background artifacts. - # macOS Aqua can flip to dark backgrounds depending on system appearance. + # Prefer native Aqua widgets on macOS; otherwise use a stable light theme try: - self.style.theme_use('clam') + if platform.system() == 'Darwin' and 'aqua' in self.style.theme_names(): + self.style.theme_use('aqua') + else: + self.style.theme_use('clam') except Exception: # Fallback to whatever default exists pass @@ -201,7 +203,6 @@ class ProleInstaller: self._register_canvas_renderer(f"dep_{dep['id']}", lambda d=dep: self._render_dependency_page(d)) self._register_canvas_renderer('build', self._render_build_page) self._register_canvas_renderer('build_summary', self._render_build_summary_page) - self._register_canvas_renderer('deploy', self._render_deploy_page) # Mirror old pages list ordering for navigation self._register_page('welcome', None) @@ -210,7 +211,6 @@ class ProleInstaller: self._register_page(f"dep_{dep['id']}", None) self._register_page('build', None) self._register_page('build_summary', None) - self._register_page('deploy', None) # Initialize page and footer self.show_page(0) @@ -435,17 +435,11 @@ class ProleInstaller: ui.render_paragraph(self, text, y, wrap) def _render_welcome_page(self): + # Opening page should NOT perform any dependency checks. self._render_title('Welcome to Prole', y=40) msg = 'Thanks for joining Prole. We will prepare your system and install the software needed to build and run Prole.' self._render_paragraph(msg, y=90) - # Status line that updates while we perform a lightweight dependency scan - status_text = 'Checking dependencies...' - self._splash_status_item = ui.canvas_text(self, 40, 130, status_text, fill='#6e6e73', font=('Helvetica', 11)) - # Start background scan once - if not self._welcome_scan_started: - self._welcome_scan_started = True - self._start_welcome_dependency_scan() - # Ensure footer reflects gating (0 buttons until scan completes) + # Ensure footer is updated (Next button visible) self.update_footer() def _start_welcome_dependency_scan(self): @@ -733,31 +727,29 @@ class ProleInstaller: current_id = self.pages[self.page_index][0] if current_id == 'deps_summary': # Determine where to go from summary - if self.verify_mode.get(): - # Start with first dependency page regardless of status - seq = self._dep_navigation_sequence(force_all=True) - target = seq[0] if seq else 'build' - self.show_page(target) - return - else: - if self.all_dependencies_installed(): - # Jump directly to build - self.show_page('build') - return - # Go to first missing dependency page - seq = self._dep_navigation_sequence() - target = seq[0] if seq else 'build' - self.show_page(target) + # Always go to the next dependency or build + if self.all_dependencies_installed(): + self.show_page('build') return + # Go to first missing dependency page + seq = self._dep_navigation_sequence() + target = seq[0] if seq else 'build' + self.show_page(target) + return if current_id == 'build': - # Treat Next as Build for Prole + # Build page button: Deploy on success (and then show summary), otherwise (re)run build + if getattr(self, '_built_success', False): + try: + self.open_drag_install_window() + except Exception: + pass + # Always navigate to Build Summary per spec + self.show_page('build_summary') + return + # Start or rerun build; Build Summary opens automatically on completion self.perform_build() return - # No longer using external Terminal; nothing special to close on summary - if current_id == 'deploy': - # Start deployment steps when moving next - self.start_deployment() - return + # No longer using external Deploy page if current_id.startswith('dep_'): # Navigate within dependency sequence seq = self._dep_navigation_sequence() @@ -793,72 +785,52 @@ class ProleInstaller: # Page-specific adjustments pid = self.pages[self.page_index][0] - # Splash/welcome gating: show 0 buttons until the dependency check completes and settles - if pid == 'welcome': - if self._splash_should_hide_nav(): - # Hide all buttons - self.prev_button.pack_forget() - self.next_button.pack_forget() - self.finish_button.pack_forget() - return - # After gating, first page should only show Next - self.prev_button.pack_forget() - self.finish_button.pack_forget() - if not self.next_button.winfo_ismapped(): - self.next_button.pack(side='right', padx=(0, 8), pady=12) + # Welcome page no longer gates navigation; treat as normal first page + if pid == 'deps_summary': + # Dependencies screens use Prev/Next wording self.next_button.configure(text='Next') - return - if pid == 'deps_summary' and self.all_dependencies_installed(): - # If verify mode is enabled, invite user to Verify instead of Build - self.next_button.configure(text='Verify' if self.verify_mode.get() else 'Build') if pid == 'build': - # Build page has Prev + Build - self.next_button.configure(text='Build') - if pid == 'deploy': - self.next_button.configure(text='Deploy') - if pid == 'build_summary': - self.next_button.configure(text='Deploy') + # Build page: show Build/Deploy/Next depending on state + if getattr(self, '_built_success', False): + self.next_button.configure(text='Deploy') + elif getattr(self, '_build_attempted', False): + # Attempted and failed → keep as Build (no Next on Build page) + self.next_button.configure(text='Build') + else: + self.next_button.configure(text='Build') # Visibility rules if first: self.prev_button.pack_forget() self.finish_button.pack_forget() if not self.next_button.winfo_ismapped(): - self.next_button.pack(side='right', padx=(0, 8), pady=12) + self.next_button.pack(side='right', padx=(0, 8), pady=10) elif last: - if not self.prev_button.winfo_ismapped(): - self.prev_button.pack(side='right', padx=(0, 8), pady=12) - self.next_button.pack_forget() - if not self.finish_button.winfo_ismapped(): - self.finish_button.pack(side='right', padx=(0, 20), pady=12) + # Special-case: Build is not the true final if we have build_summary. + # On Build page (last before summary), show Prev + Build/Deploy/Next; hide Finish. + if pid == 'build': + if not self.prev_button.winfo_ismapped(): + self.prev_button.pack(side='left', padx=(16, 8), pady=10) + if not self.next_button.winfo_ismapped(): + self.next_button.pack(side='right', padx=(0, 8), pady=10) + self.finish_button.pack_forget() + else: + if not self.prev_button.winfo_ismapped(): + self.prev_button.pack(side='left', padx=(16, 8), pady=10) + self.next_button.pack_forget() + if not self.finish_button.winfo_ismapped(): + self.finish_button.pack(side='right', padx=(0, 20), pady=10) else: # Middle pages: Prev + Next if not self.prev_button.winfo_ismapped(): - self.prev_button.pack(side='right', padx=(0, 8), pady=12) + self.prev_button.pack(side='left', padx=(16, 8), pady=10) if not self.next_button.winfo_ismapped(): - self.next_button.pack(side='right', padx=(0, 8), pady=12) + self.next_button.pack(side='right', padx=(0, 8), pady=10) self.finish_button.pack_forget() def _splash_should_hide_nav(self) -> bool: - """Return True if the welcome/splash should render zero footer buttons. - - Rules: while the splash scan is running, hide all. After it completes, - wait a small debounce so the status line "stops updating" before showing Next. - """ - try: - if self.splash_scan_running: - # Failsafe: if scan runs too long (e.g., thread stalled), allow navigation - started_at = getattr(self, 'splash_scan_started_at', None) - if started_at and (time.time() - started_at) > 8.0: - return False - return True - done_at = self.splash_scan_done_at - if not done_at: - return True - # Debounce 500ms after last update - return (time.time() - done_at) < 0.5 - except Exception: - return False + """Deprecated: Welcome page no longer performs dependency checks or gates navigation.""" + return False def _create_page_welcome(self): f = self._page_container() @@ -904,7 +876,8 @@ class ProleInstaller: return self.validation_running = True - results = {} + # Hold incremental results so we can update the status line and icons progressively + self._dep_scan_results = {} def worker(): for dep in self.dependencies: @@ -912,14 +885,47 @@ class ProleInstaller: ok, location, version = self.get_dep_info(dep) except Exception: ok, location, version = False, None, None - results[dep['id']] = (ok, location, version) - # marshal back to UI thread - self.root.after(0, lambda: self._apply_dependency_scan(results)) + did = dep['id'] + # Save and apply incrementally + self._dep_scan_results[did] = (ok, location, version) + self.root.after(0, lambda d=did, o=ok, l=location, v=version: self._apply_dependency_incremental(d, o, l, v)) + try: + time.sleep(0.02) + except Exception: + pass + # After all are processed, finalize pass to unify any remaining labels + self.root.after(0, lambda: self._apply_dependency_scan(dict(self._dep_scan_results))) t = threading.Thread(target=worker, daemon=True) self.validation_thread = t t.start() + def _apply_dependency_incremental(self, dep_id: str, ok: bool, location, version): + """Update the dependencies page row and message as each check completes.""" + if not hasattr(self, 'dep_status'): + return + slot = self.dep_status.get(dep_id) + if not slot: + return + # Update dot icon and info text for this row + self._draw_status(slot['canvas'], 'success' if ok else 'error') + if ok: + norm_ver = self.normalize_version(version) if version else '' + slot['info'].configure(text=(norm_ver or '')) + else: + slot['info'].configure(text='Not installed') + + # Update the message line with current missing list + if hasattr(self, '_dep_scan_results'): + missing = [self.dep_status[d]['dep']['name'] for d, res in self._dep_scan_results.items() if not res[0] and d in self.dep_status] + if hasattr(self, 'deps_msg') and self.deps_msg is not None: + if missing: + self.deps_msg.configure(text=f"Preparing to install ... {', '.join(missing)}") + else: + self.deps_msg.configure(text='All dependencies installed.') + # Footer may need to react if verify mode is enabled + self.update_footer() + def _apply_dependency_scan(self, results: dict): """Apply scan results to the summary UI and footer labels.""" self.validation_running = False @@ -1073,6 +1079,7 @@ class ProleInstaller: {'name': 'Tag Docker image for registry', 'status': 'pending'}, {'name': 'Push image to registry', 'status': 'pending'}, {'name': 'Import image to k3d cluster (Dev only)', 'status': 'pending'}, + {'name': 'Install LaunchAgent for port-forwards (Dev)', 'status': 'pending'}, ] self.deploy_widgets = {} container = ttk.Frame(f) @@ -1151,6 +1158,15 @@ class ProleInstaller: def perform_build(self): """Run Prole build in an embedded console (Scoped bash subprocess).""" # Prepare logs dir and file + try: + self._build_attempted = True + except Exception: + pass + # reset success flag until proven otherwise + try: + self._built_success = False + except Exception: + pass logs_dir = PROJECT_ROOT / 'logs' try: logs_dir.mkdir(parents=True, exist_ok=True) @@ -1170,19 +1186,86 @@ class ProleInstaller: guard = f'host=$(hostname -s); if [ "$host" != "{eh}" ]; then echo "ERROR: wrong host $host (expected {eh})"; exit 1; fi; ' # Simulate pressing Enter on the previewed command; do not echo a duplicate command self._console_press_enter() - full_cmd = guard + base_cmd + # Compose a verbose wrapped build command with environment diagnostics and tracing + full_cmd = guard + self._compose_verbose_build_command(base_cmd) # Run in embedded console self._run_in_console(full_cmd, self.last_build_log_path, on_complete=lambda rc: self._on_build_complete(rc)) + def _compose_verbose_build_command(self, base_cmd: str) -> str: + """Wrap the provided build command with a verbose, diagnostic-rich shell script. + + Adds: + - Timestamps and section headers + - Platform/OS/tooling info (uname, macOS version, Xcode/Swift, Java, Maven, Docker, Git) + - set -euxo pipefail for tracing and early failure + - Echo of the actual build command + """ + # Use portable bash; guard external tool probes to avoid hard failures + prologue = r''' +echo "====[PROLE] Build started $(date '+%Y-%m-%d %H:%M:%S %Z')"; +echo "---- System -------------------------------------------------------"; +uname -a || true; +printf "ARCH=%s\n" "$(uname -m)" || true; +if command -v sw_vers >/dev/null 2>&1; then sw_vers || true; fi; +echo "---- Tooling ------------------------------------------------------"; +if command -v xcodebuild >/dev/null 2>&1; then xcodebuild -version || true; else echo "xcodebuild: not found"; fi; +if command -v swift >/dev/null 2>&1; then swift --version || true; else echo "swift: not found"; fi; +if command -v java >/dev/null 2>&1; then java -version 2>&1 | sed 's/^/java: /'; else echo "java: not found"; fi; +if command -v mvn >/dev/null 2>&1; then mvn -v || true; else echo "mvn: not found"; fi; +if command -v docker >/dev/null 2>&1; then docker version || true; else echo "docker: not found"; fi; +echo "---- Git ----------------------------------------------------------"; +if command -v git >/dev/null 2>&1; then \ + (git -C "$(pwd)" rev-parse --is-inside-work-tree >/dev/null 2>&1 && \ + echo "repo: $(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")" && \ + echo "branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null)" && \ + echo "commit: $(git rev-parse --short HEAD 2>/dev/null)" && \ + git status --porcelain=v1 | sed 's/^/ /' || true) || echo "not a git repo"; \ +else echo "git: not found"; fi; +echo "-------------------------------------------------------------------"; +''' + + # The actual build with tracing and timing + wrapped = f""" +( + set -euxo pipefail + {prologue} + echo "====[PROLE] Executing build command:"; + printf '%s\n' {shlex.quote(base_cmd)}; + echo "-------------------------------------------------------------------"; + start_ts=$(date +%s || echo 0); + {base_cmd} + rc=$? + end_ts=$(date +%s || echo 0); + dur=$((end_ts - start_ts)); + echo "-------------------------------------------------------------------"; + if [ $rc -eq 0 ]; then + echo "====[PROLE] Build finished OK in ${{dur}}s at $(date '+%Y-%m-%d %H:%M:%S %Z')"; + else + echo "====[PROLE] Build FAILED (rc=$rc) in ${{dur}}s at $(date '+%Y-%m-%d %H:%M:%S %Z')"; + fi + exit $rc +) +""" + return wrapped + def _on_build_complete(self, returncode: int): - # After ProleStatus build completes, enable Next to proceed to the VM build page + # After build completes, update state and navigate to Build Summary + try: + self._built_success = (returncode == 0) + except Exception: + self._built_success = False + # If success on macOS, open the drag & drop install Finder window now + if getattr(self, '_built_success', False): + try: + self.open_drag_install_window() + except Exception: + pass + # Re-enable Next and go to summary try: - self.next_button.configure(text='Next') self.next_button.state(['!disabled']) except Exception: pass - # Navigate directly to the build summary page self.show_page('build_summary') # ---------------- Embedded console helpers ---------------- @@ -1633,19 +1716,61 @@ class ProleInstaller: # ---------------- Build Summary page ---------------- def _render_build_summary_page(self): - self._render_title('Build summary', y=40) + # Build Summary: show combined stdout/stderr from last build log + self._render_title('Build Summary', y=40) + if getattr(self, '_built_success', False): + self._render_paragraph('✅ Build completed successfully. You can now drag Prole.app into Applications. Output below:', y=90) + else: + # Troubleshooting header + tips = ( + "❌ Build failed. Troubleshooting tips:\n" + "• Ensure Xcode Command Line Tools are installed: xcode-select --install\n" + "• Verify Swift toolchain and SPM networking (try again; network hiccups can happen)\n" + "• If on Apple Silicon, ensure dependencies target arm64 or install via Homebrew\n" + "• Clean derived data / SPM cache if needed: rm -rf ~/Library/Developer/Xcode/DerivedData\n" + "• Check Docker status if Docker-related steps are used\n" + ) + self._render_paragraph(tips, y=90) logp = getattr(self, 'last_build_log_path', None) + # Place a scrolled text overlay to show the log + try: + # Use same geometry helper as console but with DARK theme (match Build console) + l, t, w, h = self._compute_console_geometry(120) + txt = scrolledtext.ScrolledText(self.slide_area, wrap='word', + bg='#000000', fg='#ffffff', insertbackground='#ffffff', + font=('Menlo', 11), relief='flat', bd=0, highlightthickness=0) + txt.place(x=l, y=t, width=max(200, w), height=max(140, h)) + self._overlay_widgets.append(txt) + if logp and os.path.exists(logp): + try: + with open(logp, 'r', encoding='utf-8', errors='ignore') as fp: + content = fp.read() + txt.insert('1.0', content) + except Exception as e: + txt.insert('1.0', f"Failed to read log: {e}\n") + else: + txt.insert('1.0', 'No build log available.') + txt.configure(state='disabled') + # Keep overlay positioned on resize + def _on_resize(_evt=None): + try: + l2, t2, w2, h2 = self._compute_console_geometry(120) + txt.place(x=l2, y=t2, width=max(200, w2), height=max(140, h2)) + except Exception: + pass + self._overlay_bind_id = self.slide_area.bind('', _on_resize) + except Exception: + # Fallback: just show path + self._render_paragraph('No build log could be displayed.', y=120) + # Add link to open the log file in Finder/TextEdit + y_links = 90 + 24 if logp: - self._render_paragraph('Build output was captured from the embedded console.', y=90) - self._render_paragraph('Prole build log:', y=120) - self._render_paragraph(logp, y=140) - link_y = 200 - link1 = ui.render_link(self, 56, link_y, 'Open Prole build log') - self._canvas_items.append(link1) + link = ui.render_link(self, 56, y_links, 'Open build log file') + self._canvas_items.append(link) def _open_log(event): ex, ey = event.x, event.y - bbox = self.bg_canvas.bbox(link1) + bbox = self.bg_canvas.bbox(link) if bbox and bbox[0] <= ex <= bbox[2] and bbox[1] <= ey <= bbox[3]: try: if platform.system() == 'Darwin': @@ -1655,8 +1780,94 @@ class ProleInstaller: except Exception: pass self.bg_canvas.bind('', _open_log) - else: - self._render_paragraph('No build log path available.', y=100) + + # On success, also offer to open the dist folder + if getattr(self, '_built_success', False): + try: + dist_dir = str(self._get_prole_dist_dir()) + except Exception: + dist_dir = None + if dist_dir: + y_links += 24 + link2 = ui.render_link(self, 56, y_links, 'Open dist folder') + self._canvas_items.append(link2) + + def _open_dist(event): + ex, ey = event.x, event.y + bbox = self.bg_canvas.bbox(link2) + if bbox and bbox[0] <= ex <= bbox[2] and bbox[1] <= ey <= bbox[3]: + try: + if platform.system() == 'Darwin': + subprocess.run(['open', dist_dir]) + else: + webbrowser.open(f'file://{dist_dir}') + except Exception: + pass + self.bg_canvas.bind('', _open_dist) + + # ---------------- Drag-and-drop install (macOS Finder) ---------------- + def _get_prole_dist_dir(self) -> Path: + return PROJECT_ROOT / 'prole-app' / 'dist' + + def ensure_applications_symlink(self): + try: + dist = self._get_prole_dist_dir() + dist.mkdir(parents=True, exist_ok=True) + link_path = dist / 'Applications' + target = Path('/Applications') + if link_path.exists() or link_path.is_symlink(): + # If an existing correct symlink, leave as-is + try: + if link_path.is_symlink() and Path(os.readlink(link_path)) == target: + return + except Exception: + pass + # Otherwise attempt to remove and recreate + try: + if link_path.is_dir() and not link_path.is_symlink(): + # do not remove a real Applications directory accidentally + return + link_path.unlink(missing_ok=True) + except Exception: + pass + os.symlink(str(target), str(link_path)) + except Exception: + pass + + def open_drag_install_window(self): + """Open a Finder window to the dist folder with an Applications symlink for drag-and-drop install.""" + if platform.system() != 'Darwin': + # Non-macOS: do nothing + return + try: + dist = self._get_prole_dist_dir() + app_path = dist / 'Prole.app' + # Ensure symlink exists + self.ensure_applications_symlink() + # Open Finder to the dist folder + osa = f''' + tell application "Finder" + activate + try + set theFolder to POSIX file "{str(dist)}" as alias + make new Finder window to theFolder + set current view of front Finder window to icon view + set toolbar visible of front Finder window to true + set statusbar visible of front Finder window to true + select {{}} + end try + end tell + ''' + subprocess.run(['osascript', '-e', osa]) + # If app exists, also reveal it + if app_path.exists(): + subprocess.run(['open', '-R', str(app_path)]) + except Exception: + try: + # Fallback: open the folder normally + subprocess.run(['open', str(self._get_prole_dist_dir())]) + except Exception: + pass def create_install_screen(self): """Create the dependency installer screen""" @@ -2100,12 +2311,156 @@ class ProleInstaller: self.update_deploy_step_status('Import image to k3d cluster (Dev only)', 'completed') else: self.update_deploy_step_status('Import image to k3d cluster (Dev only)', 'skipped') + + # Step 7: Install LaunchAgent for port-forwards (Dev) + if env == 'Dev': + self.update_deploy_step_status('Install LaunchAgent for port-forwards (Dev)', 'running') + ok = self.install_launchagent_port_forwards() + if ok: + self.update_deploy_step_status('Install LaunchAgent for port-forwards (Dev)', 'completed') + else: + self.update_deploy_step_status('Install LaunchAgent for port-forwards (Dev)', 'error') + return + else: + self.update_deploy_step_status('Install LaunchAgent for port-forwards (Dev)', 'skipped') messagebox.showinfo("Success", "Deployment completed successfully!") except Exception as e: messagebox.showerror("Error", f"Deployment failed: {str(e)}") + def install_launchagent_port_forwards(self) -> bool: + """Create/update the user LaunchAgent and helper script to manage kubectl port-forwards. + - Helper script: ~/Library/Application Support/Prole/bin/prole-kpf.sh + - LaunchAgent: ~/Library/LaunchAgents/org.prole.prole-db.kpf-dev.plist + """ + try: + home = Path.home() + bin_dir = home / 'Library' / 'Application Support' / 'Prole' / 'bin' + run_dir = home / 'Library' / 'Application Support' / 'Prole' / 'run' + plist_path = home / 'Library' / 'LaunchAgents' / 'org.prole.prole-db.kpf-dev.plist' + script_path = bin_dir / 'prole-kpf.sh' + + bin_dir.mkdir(parents=True, exist_ok=True) + run_dir.mkdir(parents=True, exist_ok=True) + plist_path.parent.mkdir(parents=True, exist_ok=True) + + helper_script = """#!/bin/sh +set -eu + +LABEL="org.prole.prole-db.kpf-dev" +PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" +RUNDIR="$HOME/Library/Application Support/Prole/run" +PIDFILE="$RUNDIR/kpf.pids" + +ensure_rundir() { + mkdir -p "$RUNDIR" +} + +list_cmds() { + # Enumerate ProleCommands array using PlistBuddy + if /usr/libexec/PlistBuddy -c "Print :ProleCommands" "$PLIST" >/dev/null 2>&1; then + i=0 + while true; do + if ! val=$(/usr/libexec/PlistBuddy -c "Print :ProleCommands:$i" "$PLIST" 2>/dev/null); then + break + fi + echo "$val" + i=$((i+1)) + done + fi +} + +start() { + ensure_rundir + : > "$PIDFILE" + IFS='\n' + for cmd in $(list_cmds); do + [ -z "$cmd" ] && continue + (sh -lc "$cmd") & + echo $! >> "$PIDFILE" + done + wait +} + +stop() { + if [ -f "$PIDFILE" ]; then + while read -r pid; do + [ -z "$pid" ] && continue + kill "$pid" 2>/dev/null || true + done < "$PIDFILE" + rm -f "$PIDFILE" + fi +} + +status() { + if [ ! -f "$PIDFILE" ]; then + echo "not running" + exit 3 + fi + alive=0 + total=0 + while read -r pid; do + [ -z "$pid" ] && continue + total=$((total+1)) + if kill -0 "$pid" 2>/dev/null; then alive=$((alive+1)); fi + done < "$PIDFILE" + echo "$alive/$total running" +} + +case "${1:-}" in + start) start ;; + stop) stop ;; + restart) stop; start ;; + status) status ;; + *) echo "Usage: $0 {start|stop|restart|status}" >&2; exit 2 ;; +esac +""" + + script_path.write_text(helper_script) + os.chmod(script_path, 0o755) + + # Default commands for Dev environment (k3d) + default_cmds = [ + "kubectl port-forward svc/prometheus-community-kube-prometheus 9090", + "kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard-kong-proxy 8443:443", + "kubectl port-forward svc/prole-db-rw 5432:5432 --address 0.0.0.0", + "kubectl port-forward svc/prometheus-community-grafana 3000:80 --address 0.0.0.0", + ] + + # Create plist dict + from plistlib import dumps as plist_dumps + plist_dict = { + 'Label': 'org.prole.prole-db.kpf-dev', + 'RunAtLoad': True, + 'KeepAlive': True, + # Ensure PATH has common locations for kubectl + 'EnvironmentVariables': { + 'PATH': os.environ.get('PATH', '/usr/local/bin:/usr/bin:/bin') + }, + 'StandardOutPath': str(home / 'Library' / 'Logs' / 'org.prole.prole-db.kpf-dev.out.log'), + 'StandardErrorPath': str(home / 'Library' / 'Logs' / 'org.prole.prole-db.kpf-dev.err.log'), + 'ProgramArguments': [str(script_path), 'start'], + 'ProleCommands': default_cmds, + } + + plist_bytes = plist_dumps(plist_dict) + plist_path.write_bytes(plist_bytes) + + # Reload the agent + uid = os.getuid() + # Try to kickstart first + subprocess.run(['launchctl', 'kickstart', '-k', f'gui/{uid}/org.prole.prole-db.kpf-dev'], capture_output=True) + # Bootout and bootstrap to ensure it's loaded, then kickstart + subprocess.run(['launchctl', 'bootout', f'gui/{uid}', f'gui/{uid}/org.prole.prole-db.kpf-dev'], capture_output=True) + subprocess.run(['launchctl', 'bootstrap', f'gui/{uid}', str(plist_path)], check=True) + subprocess.run(['launchctl', 'kickstart', '-k', f'gui/{uid}/org.prole.prole-db.kpf-dev'], check=True) + + return True + except Exception as e: + print(f"LaunchAgent setup failed: {e}") + return False + def check_xcode_tools(self): """Check if Xcode Command Line Tools are installed via deploy helper.""" diff --git a/installer/build.py b/installer/build.py index e3809a0..ed52668 100644 --- a/installer/build.py +++ b/installer/build.py @@ -16,11 +16,11 @@ def get_build_command(project_root: Path, env: str | None) -> str: """Return the Prole app build command with an env comment suffix. Example output: - cd "/path/to/repo" && ./prole-app/build.sh build # Dev + cd "/path/to/repo" && PROLE_VERBOSE=1 ./prole-app/build.sh build --verbose # Dev """ env_label = (env or "Dev").strip().title() prole_root = str(project_root) - return f"cd \"{prole_root}\" && ./prole-app/build.sh build # {env_label}" + return f"cd \"{prole_root}\" && PROLE_VERBOSE=1 ./prole-app/build.sh build --verbose # {env_label}" # ---------------- Screen (UI) helpers ---------------- diff --git a/installer/screen.py b/installer/screen.py index 60285b6..427b1bb 100644 --- a/installer/screen.py +++ b/installer/screen.py @@ -151,19 +151,15 @@ def create_nav_footer(parent, buttons: list[tuple[int, str]], commands: dict[int # Ensure a consistent style for navigation buttons try: style = ttk.Style() - # Safe to call configure multiple times; last one wins - style.configure(style_name, padding=(12, 6), font=('Helvetica', 12)) + # Keep padding modest to allow Aqua to size buttons naturally. + # Avoid forcing a font so the native Aqua metrics are used. + style.configure(style_name, padding=(10, 6)) except Exception: pass footer = ttk.Frame(parent) footer.pack(fill='x', side='bottom') - # Ensure stable geometry even when there are no buttons - try: - footer.configure(height=60) - footer.pack_propagate(False) - except Exception: - pass + # Let the footer naturally size to its contents to avoid clipping text on macOS # Flexible spacer to push buttons to the right spacer = ttk.Frame(footer) @@ -177,7 +173,8 @@ def create_nav_footer(parent, buttons: list[tuple[int, str]], commands: dict[int # Right-aligned order (pack to the right in the declared order) # Small extra right padding on the last (right-most) button pad = (0, 20) if title.lower() in ('finish', 'exit', 'done') else (0, 8) - b.pack(side='right', padx=pad, pady=12) + # Use modest vertical padding; let Aqua compute proper height. + b.pack(side='right', padx=pad, pady=10) btn_map[btn_id] = b # Return both the frame and button map if needed later by callers diff --git a/prole-app/Sources/AppDelegate.swift b/prole-app/Sources/AppDelegate.swift index 9efaf0c..782cd70 100644 --- a/prole-app/Sources/AppDelegate.swift +++ b/prole-app/Sources/AppDelegate.swift @@ -21,7 +21,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private var statusItemController: StatusItemController! // Splash screen removed — keep no reference private var appMenuToggleStatusBarItem: NSMenuItem? - private var pfManager: PortForwardManager? + // Port-forwarding is now managed by a user LaunchAgent. Keep a small helper. + private let launchAgentManager = LaunchAgentManager() private var preferencesWindowController: PreferencesWindowController? // We keep the app in one of two simple modes. @@ -40,21 +41,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // Splash removed: start directly + // Ensure helper script exists for LaunchAgent + launchAgentManager.ensureHelperScript() + // ServiceChecker does the lightweight TCP checks on a background timer serviceChecker = ServiceChecker() dlog("ServiceChecker created") - // Optional: Port-forward supervision (Dev) - if Config.shared.pfEnabled { - let cmds = Config.shared.pfCommands - if !cmds.isEmpty { - let mgr = PortForwardManager(commands: cmds) - self.pfManager = mgr - mgr.start() - } - } + // Note: In-app port-forward supervision has been removed. + // Port-forwards are handled by LaunchAgents. Nothing to start here. // Overlay (Status Bar mode) — a thin, mostly click‑through window near the top - overlayWindowController = OverlayWindowController(serviceChecker: serviceChecker, pfManager: pfManager) + overlayWindowController = OverlayWindowController(serviceChecker: serviceChecker, pfManager: nil) dlog("OverlayWindowController created; computing initial frame/visibility") overlayWindowController.showIfMenuBarVisible() @@ -62,7 +59,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { mainWindowController = MainWindowController(serviceChecker: serviceChecker, actions: .init( onRefresh: { [weak self] in self?.refreshNow() }, onMinimizeToStatusBar: { [weak self] in self?.switchToStatusBarMode() } - ), pfManager: pfManager) + ), pfManager: nil) dlog("MainWindowController created") // Workstation window (VNC viewer) — shown to the right of Prole Status when not in status bar mode @@ -235,13 +232,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate { appMenu.addItem(NSMenuItem.separator()) - // Reset Port Forwards (Dev) - if pfManager != nil { - let resetPF = NSMenuItem(title: "Reset Port Forwards", action: #selector(menuResetPortForwards), keyEquivalent: "") - resetPF.target = self - appMenu.addItem(resetPF) - appMenu.addItem(NSMenuItem.separator()) - } + // Restart Port Forwards (delegates to etc/init-port-fowards.sh) + let resetPF = NSMenuItem(title: "Restart Port Forwards", action: #selector(menuResetPortForwards), keyEquivalent: "") + resetPF.target = self + appMenu.addItem(resetPF) + appMenu.addItem(NSMenuItem.separator()) // Refresh let refresh = NSMenuItem(title: "Refresh Now", action: #selector(menuRefresh), keyEquivalent: "r") @@ -268,7 +263,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { @objc private func menuToggleMode() { toggleMode() } @objc private func menuRefresh() { refreshNow() } - @objc private func menuResetPortForwards() { pfManager?.reset() } + @objc private func menuResetPortForwards() { + _ = PFScriptBridge.restart() + } @objc private func menuPreferences() { if preferencesWindowController == nil { preferencesWindowController = PreferencesWindowController() diff --git a/prole-app/Sources/MainWindowController.swift b/prole-app/Sources/MainWindowController.swift index a24f23a..3d157af 100644 --- a/prole-app/Sources/MainWindowController.swift +++ b/prole-app/Sources/MainWindowController.swift @@ -22,7 +22,6 @@ final class MainWindowController: NSWindowController { tf.translatesAutoresizingMaskIntoConstraints = false return tf }() - private var pfManager: PortForwardManager? private let actions: Actions init(serviceChecker: ServiceChecker, actions: Actions, pfManager: PortForwardManager? = nil) { @@ -112,11 +111,6 @@ final class MainWindowController: NSWindowController { ]) statusView.bindTo(serviceChecker: serviceChecker) - self.pfManager = pfManager - statusView.setPortForwardManager(pfManager) - if let m = pfManager { - NotificationCenter.default.addObserver(self, selector: #selector(updatePFDetails), name: PortForwardManager.statusDidChangeNotification, object: m) - } updatePFDetails() } @@ -150,35 +144,21 @@ final class MainWindowController: NSWindowController { } // MARK: - Actions - @objc private func didTapRefresh() { actions.onRefresh() } + @objc private func didTapRefresh() { + updatePFDetails() + actions.onRefresh() + } @objc private func didTapMinimize() { actions.onMinimizeToStatusBar() } - // MARK: - Port Forward Details + // MARK: - Port Forward Details (from init-port-fowards.sh) @objc private func updatePFDetails() { - guard let m = pfManager else { detailsLabel.stringValue = ""; return } - if m.details.isEmpty { - if Config.shared.pfEnabled { - detailsLabel.stringValue = "Port Forwards: enabled but no commands loaded (check prole.properties path)" - } else { - detailsLabel.stringValue = "Port Forwards: none configured" - } - return + let res = PFScriptBridge.status() + if res.code == 0 { + let trimmed = res.out.trimmingCharacters(in: .whitespacesAndNewlines) + detailsLabel.stringValue = trimmed.isEmpty ? "init-port-fowards.sh: no status" : trimmed + } else { + let err = res.err.trimmingCharacters(in: .whitespacesAndNewlines) + detailsLabel.stringValue = "init-port-fowards.sh status error (code \(res.code))\n\(err)" } - var lines: [String] = ["Port Forwards:"] - let df = DateFormatter(); df.dateStyle = .short; df.timeStyle = .medium - for d in m.details { - let pid = d.pid != nil ? String(d.pid!) : "-" - let status = d.running ? "running" : "stopped" - var extra = "" - if !d.running { - if let code = d.lastExitCode, let when = d.lastExitAt { - extra = " (exit=\(code) at \(df.string(from: when)))" - } else if let code = d.lastExitCode { - extra = " (exit=\(code))" - } - } - lines.append(" • PID \(pid) • \(status)\(extra) — \(d.command)") - } - detailsLabel.stringValue = lines.joined(separator: "\n") } } diff --git a/prole-app/Sources/OverlayWindow.swift b/prole-app/Sources/OverlayWindow.swift index c1c6455..84d9ee3 100644 --- a/prole-app/Sources/OverlayWindow.swift +++ b/prole-app/Sources/OverlayWindow.swift @@ -2,7 +2,6 @@ import AppKit final class OverlayWindowController: NSWindowController { private let statusView = StatusView() - private var pfManager: PortForwardManager? // Fully transparent background; no visual effect overlay init(serviceChecker: ServiceChecker, pfManager: PortForwardManager? = nil) { @@ -33,8 +32,6 @@ final class OverlayWindowController: NSWindowController { ]) statusView.bindTo(serviceChecker: serviceChecker) - self.pfManager = pfManager - statusView.setPortForwardManager(pfManager) // Initial placement recomputeFrameAndVisibility() @@ -106,9 +103,7 @@ final class OverlayWindowController: NSWindowController { menu.addItem(withTitle: statusView.aggregateStatusSummary(), action: nil, keyEquivalent: "") menu.addItem(.separator()) menu.addItem(withTitle: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r").target = self - if pfManager != nil { - menu.addItem(withTitle: "Reset Port Forwards", action: #selector(resetPortForwards), keyEquivalent: "").target = self - } + menu.addItem(withTitle: "Restart Port Forwards", action: #selector(resetPortForwards), keyEquivalent: "").target = self menu.addItem(.separator()) menu.addItem(withTitle: "Quit Prole", action: #selector(quit), keyEquivalent: "q").target = self @@ -131,7 +126,7 @@ final class OverlayWindowController: NSWindowController { } @objc private func resetPortForwards() { - pfManager?.reset() + _ = PFScriptBridge.restart() } } diff --git a/prole-app/Sources/StatusView.swift b/prole-app/Sources/StatusView.swift index 8a40861..af08154 100644 --- a/prole-app/Sources/StatusView.swift +++ b/prole-app/Sources/StatusView.swift @@ -4,7 +4,6 @@ final class StatusView: NSView { private let light1 = TrafficLight() private let light2 = TrafficLight() private let light3 = TrafficLight() - private let lightPF = TrafficLight() private let timestampLabel: NSTextField = { let tf = NSTextField(labelWithString: "0000-00-00 00:00:00 +00:00") tf.textColor = .secondaryLabelColor @@ -47,7 +46,7 @@ final class StatusView: NSView { wantsLayer = true translatesAutoresizingMaskIntoConstraints = false - let stack = NSStackView(views: [light1, light2, light3, lightPF, timestampLabel, marquee, maximizeButton]) + let stack = NSStackView(views: [light1, light2, light3, timestampLabel, marquee, maximizeButton]) stack.orientation = .horizontal stack.alignment = .centerY stack.distribution = .equalSpacing @@ -84,22 +83,12 @@ final class StatusView: NSView { return nil } - private var pfManager: PortForwardManager? - func bindTo(serviceChecker: ServiceChecker) { self.checker = serviceChecker NotificationCenter.default.addObserver(self, selector: #selector(updateLights), name: ServiceChecker.statusDidChangeNotification, object: serviceChecker) updateLights() } - func setPortForwardManager(_ manager: PortForwardManager?) { - self.pfManager = manager - if let m = manager { - NotificationCenter.default.addObserver(self, selector: #selector(updateLights), name: PortForwardManager.statusDidChangeNotification, object: m) - } - updateLights() - } - @objc private func updateLights() { guard let c = checker else { return } // Light 1: svc.prole.org:443 @@ -115,15 +104,6 @@ final class StatusView: NSView { light3.state = c.localReachable ? .green : .red light3.toolTip = c.tooltipForLocal() - // Light 4: port-forward supervision status (Dev) - if let m = pfManager { - lightPF.state = m.allRunning ? .green : .red - lightPF.toolTip = m.allRunning ? "kubectl port-forwards: running" : "kubectl port-forwards: not all running" - } else { - lightPF.state = .red - lightPF.toolTip = "kubectl port-forwards: disabled" - } - // Timestamp label let now = Date() timestampLabel.stringValue = timeFormatter.string(from: now) diff --git a/prole-app/build.sh b/prole-app/build.sh index 203bb6d..3326fa9 100755 --- a/prole-app/build.sh +++ b/prole-app/build.sh @@ -46,6 +46,7 @@ Commands: Options (for build): --arch Build for a specific arch (arm64 or x86_64). Defaults to host arch. + --verbose Print verbose build commands (same as PROLE_VERBOSE=1) Examples: ./build.sh build @@ -60,6 +61,7 @@ Dependencies: Environment overrides (optional): ROYALVNCKIT_XCFRAMEWORK If set, use this RoyalVNCKit.xcframework instead of building KEEP_DEPS=1 Keep the deps staging directory after the build (for debugging) + PROLE_VERBOSE=1 Stream verbose output from SwiftPM/xcodebuild and echo commands EOF } @@ -67,6 +69,42 @@ ensure_dirs() { mkdir -p "$BUILD_DIR" "$DIST_DIR" "$MACOS_DIR" "$RESOURCES_DIR" "$CONTENTS_DIR/Frameworks" "$DEPS_SRC_DIR" "$DEPS_OUT_DIR" "$LOG_DIR" "$DEPS_LOGS_DIR" } +# Determine verbosity from env/flags +VERBOSE="${PROLE_VERBOSE:-0}" +if [[ "$VERBOSE" = "1" ]]; then + # Enable shell tracing for more insight + set -x +fi + +# Resolve a SwiftPM-capable command (prefer xcrun swift on macOS) +resolve_swiftpm() { + if command -v xcrun >/dev/null 2>&1 && xcrun swift --version >/dev/null 2>&1; then + echo "xcrun swift" + return 0 + fi + if command -v swift >/dev/null 2>&1; then + if swift build --help >/dev/null 2>&1; then + echo "swift" + return 0 + fi + fi + # Fallback to package form via xcrun if available + if command -v xcrun >/dev/null 2>&1 && xcrun swift package --help >/dev/null 2>&1; then + echo "xcrun swift" + return 0 + fi + return 1 +} + +SWIFTPM_CMD=$(resolve_swiftpm || true) +if [[ -z "${SWIFTPM_CMD:-}" ]]; then + echo "[spm] error: Swift Package Manager not found. Install Xcode Command Line Tools: xcode-select --install" >&2 + xcodebuild -version 2>/dev/null || true + swift --version 2>/dev/null || true + exit 1 +fi +echo "[spm] Using SwiftPM: ${SWIFTPM_CMD}" + # Read a key from prole.properties (very simple parser) prop_get() { local key="$1" @@ -439,11 +477,28 @@ spm_build_binary() { pushd "$ROOT_DIR" >/dev/null || return 1 mkdir -p "$LOG_DIR" # Build via SwiftPM (Package.swift in prole-app). All dependencies managed by SPM. - local args=( build -c release --arch "$arch" \ - -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" \ - ) - # Run swift build quietly; write all output to the log to avoid contaminating stdout - swift "${args[@]}" >"$LOG_DIR/spm-build-${arch}.log" 2>&1 || { popd >/dev/null; return 1; } + # Prepare argument arrays to avoid word-splitting issues + local spm_verbose_flag=() + if [[ "$VERBOSE" = "1" ]]; then + spm_verbose_flag=( -v ) + fi + local build_flags=( -c release --arch "$arch" \ + -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" ) + + # Turn SWIFTPM_CMD (e.g., "xcrun swift") into an array + local -a SWIFTPM_ARR + # shellcheck disable=SC2206 + SWIFTPM_ARR=( $SWIFTPM_CMD ) + + # If verbose, stream output to stdout and tee to log; otherwise, write only to log + if [[ "$VERBOSE" = "1" ]]; then + echo "[spm] Executing: ${SWIFTPM_CMD} build ${spm_verbose_flag[*]} ${build_flags[*]}" + "${SWIFTPM_ARR[@]}" build "${spm_verbose_flag[@]}" "${build_flags[@]}" 2>&1 | tee "$LOG_DIR/spm-build-${arch}.log" + local rc=${PIPESTATUS[0]} + if [[ $rc -ne 0 ]]; then popd >/dev/null; return $rc; fi + else + "${SWIFTPM_ARR[@]}" build "${build_flags[@]}" >"$LOG_DIR/spm-build-${arch}.log" 2>&1 || { popd >/dev/null; return 1; } + fi popd >/dev/null local candidate1="$ROOT_DIR/.build/${arch}-apple-macosx/release/${APP_NAME}" local candidate2="$ROOT_DIR/.build/release/${APP_NAME}" @@ -585,9 +640,11 @@ case "$cmd" in clean ;; build) arch="${ARCH_CURRENT}" + # Allow per-invocation verbose flag while [ $# -gt 0 ]; do case "$1" in --arch) arch="$2"; shift 2 ;; + --verbose) VERBOSE="1"; shift ;; *) echo "Unknown option: $1"; usage; exit 1 ;; esac done diff --git a/prole-db/bulk_insert.sql b/prole-db/bulk_insert.sql index a36a2e4..445ec17 100644 --- a/prole-db/bulk_insert.sql +++ b/prole-db/bulk_insert.sql @@ -1,4 +1,4 @@ -BULK INSERT dbo.wells +BULK INSERT prole.dbo.wells FROM '/var/opt/mssql/data/2023CaliforniaOilAndGasWells.csv' WITH ( FIRSTROW = 2, diff --git a/mssql/Dockerfile b/prole-mssql-db/Dockerfile similarity index 100% rename from mssql/Dockerfile rename to prole-mssql-db/Dockerfile diff --git a/mssql/db-service.yaml b/prole-mssql-db/db-service.yaml similarity index 100% rename from mssql/db-service.yaml rename to prole-mssql-db/db-service.yaml diff --git a/mssql/prole-mssql-db-deployment.yaml b/prole-mssql-db/prole-mssql-db-deployment.yaml similarity index 100% rename from mssql/prole-mssql-db-deployment.yaml rename to prole-mssql-db/prole-mssql-db-deployment.yaml diff --git a/mssql/prole-mssql-db-iscsi-data001.yaml b/prole-mssql-db/prole-mssql-db-iscsi-data001.yaml similarity index 100% rename from mssql/prole-mssql-db-iscsi-data001.yaml rename to prole-mssql-db/prole-mssql-db-iscsi-data001.yaml diff --git a/mssql/prole-mssql-db-nfs-data001.yaml b/prole-mssql-db/prole-mssql-db-nfs-data001.yaml similarity index 100% rename from mssql/prole-mssql-db-nfs-data001.yaml rename to prole-mssql-db/prole-mssql-db-nfs-data001.yaml diff --git a/mssql/prole-mssql-db-nfs-deployment.yaml b/prole-mssql-db/prole-mssql-db-nfs-deployment.yaml similarity index 100% rename from mssql/prole-mssql-db-nfs-deployment.yaml rename to prole-mssql-db/prole-mssql-db-nfs-deployment.yaml