nandi committed 1 months ago
deleted file mode 120000-/nix/store/7qs31js7dw2ayj0q807v9apmwbw00jvb-cowsay-3.8.4\ No newline at end of file
deleted file mode 120000-/nix/store/ps0aal328bh5xlf861vx3dx3lcm2qibn-cowsay-3.8.4-man\ No newline at end of file
return f"{bytes_val:.1f}{unit}" bytes_val /= 1024 return f"{bytes_val:.1f}TB"++ def _transfer_label(+ self,+ color: str,+ prefix: str,+ name: str,+ downloaded: int,+ total: Optional[int],+ ) -> str:+ """Build a transfer label that includes byte counters when known."""+ if total and total > 0:+ return (+ f" [{color}]{prefix} {name[:20]} "+ f"({self.format_size(downloaded)}/{self.format_size(total)})[/]"+ )+ if downloaded > 0:+ return f" [{color}]{prefix} {name[:20]} ({self.format_size(downloaded)})[/]"+ return f" [{color}]{prefix} {name[:25]}[/]" def render_state( self, ) # Overall build progress- total_tasks = state.total_builds + state.total_downloads- done_tasks = state.completed_builds + state.completed_downloads- running_tasks = len(state.running_builds) + len(state.running_downloads)+ total_tasks = state.total_builds + state.total_downloads + state.total_uploads+ done_tasks = state.completed_builds + state.completed_downloads + state.completed_uploads+ running_tasks = len(state.running_builds) + len(state.running_downloads) + len(state.running_uploads) if total_tasks > 0: overall_task = progress.add_task( if dep: if dep.size and dep.size > 0: progress.add_task(- f" [blue]DL {name[:25]}[/]",+ self._transfer_label("blue", "DL", name, dep.downloaded, dep.size), total=dep.size, completed=dep.downloaded, ) else: progress.add_task(- f" [blue]DL {name[:25]}[/]",+ self._transfer_label("blue", "DL", name, dep.downloaded, dep.size),+ total=None,+ )+ + # Running uploads (copying source)+ for name in sorted(state.running_uploads):+ dep = state.dependencies.get(name)+ if dep:+ if dep.size and dep.size > 0:+ # For path-based uploads, show path count+ if 'paths' in name:+ progress.add_task(+ f" [magenta]UP {name[:25]}[/]",+ total=dep.size,+ completed=dep.downloaded,+ )+ else:+ # For byte-based uploads, show size in the label+ progress.add_task(+ self._transfer_label("magenta", "UP", name, dep.downloaded, dep.size),+ total=dep.size,+ completed=dep.downloaded,+ )+ else:+ progress.add_task(+ self._transfer_label("magenta", "UP", name, dep.downloaded, dep.size), total=None, ) lines.append(f"[dim]{state.status_message}[/]") lines.append("") + # Show active upload/download progress summary+ if state.running_downloads:+ for name in sorted(state.running_downloads):+ dep = state.dependencies.get(name)+ if dep and dep.downloaded > 0:+ if dep.size and dep.size > 0:+ lines.append(+ f" [blue]↓ {self.format_size(dep.downloaded)}/{self.format_size(dep.size)}[/]"+ )+ else:+ lines.append(f" [blue]↓ {self.format_size(dep.downloaded)}[/]")+ lines.append("")++ if state.running_uploads:+ for name in sorted(state.running_uploads):+ dep = state.dependencies.get(name)+ if dep:+ if dep.size and dep.downloaded > 0:+ # For path-based uploads, show "X/Y paths"+ if 'paths' in name:+ lines.append(f" [magenta]↑ {dep.downloaded}/{dep.size} paths[/]")+ else:+ # For byte-based uploads, show size progress+ lines.append(f" [magenta]↑ {self.format_size(dep.downloaded)}/{self.format_size(dep.size)}[/]")+ elif dep.downloaded > 0:+ # Known transferred but unknown total+ lines.append(f" [magenta]↑ {self.format_size(dep.downloaded)}[/]")+ lines.append("")+ recent_events = state.recent_events[-8:] if show_recent_events and recent_events: lines.append("[dim]Recent activity:[/]") # Completed counts completed_builds: int = 0 completed_downloads: int = 0+ completed_uploads: int = 0 failed_builds: int = 0 @property return len([d for d in self.dependencies.values() if d.activity_type == ActivityType.DOWNLOAD]) + @property+ def total_uploads(self) -> int:+ """Total number of uploads."""+ return len([d for d in self.dependencies.values() + if d.activity_type == ActivityType.UPLOAD])+ def add_dependency(self, dep: Dependency) -> None: """Add a dependency to tracking.""" self.dependencies[dep.name] = dep self.completed_builds += 1 elif dep.activity_type == ActivityType.DOWNLOAD: self.completed_downloads += 1+ elif dep.activity_type == ActivityType.UPLOAD:+ self.completed_uploads += 1 elif status == BuildStatus.FAILED: self.failed_builds += 1
DOWNLOAD_RE = re.compile( r"(downloading|fetching).*'(/nix/store/[^']+)'" )- COPYING_RE = re.compile(r"copying (path|signal).*'(/nix/store/[^']+)'")+ COPYING_RE = re.compile(r"copying (path|signal|source).*'([^']+)'")+ COPYING_TO_RE = re.compile(r"copying.*to ['\"]?([^'\">\s]+)") def __init__(self, use_json: bool = False): self.use_json = use_json self._result_data: dict[int, dict] = {} # activity id -> result data for downloads self._pending_builders: dict[str, str] = {} # dep name -> builder host/name + def _coerce_transfer_int(self, value: object) -> Optional[int]:+ """Return a non-negative transfer counter from a JSON field."""+ if isinstance(value, bool) or not isinstance(value, int):+ return None+ return max(value, 0)++ def _mark_failed_builds(self, message: str) -> None:+ """Mark builds as failed based on an error message."""+ matched = False++ for path in re.findall(r"(/nix/store/[^'\s]+\.drv)", message):+ name = self._extract_name(path)+ if name in self.state.dependencies:+ self.state.update_status(name, BuildStatus.FAILED, datetime.now())+ matched = True++ if matched:+ return++ for name in list(self.state.running_builds):+ self.state.update_status(name, BuildStatus.FAILED, datetime.now())++ def _track_transfer_start(+ self,+ activity_id: int,+ source: str,+ destination: str,+ text: str = "",+ ) -> Optional[str]:+ """Create a tracked upload/download from generic transfer metadata."""+ if not source or not destination:+ return None++ is_upload = "://" in destination+ activity = ActivityType.UPLOAD if is_upload else ActivityType.DOWNLOAD++ if source.startswith("/nix/store/"):+ name = self._extract_name(source)+ out_path = source+ else:+ name = source.split("/")[-1][:40] if "/" in source else source[:40]+ out_path = source++ if not self._should_track_activity(name, activity, out_path):+ return None++ dep = Dependency(+ name=name,+ out_path=out_path,+ status=BuildStatus.RUNNING,+ activity_type=activity,+ started_at=datetime.now(),+ )+ self.state.add_dependency(dep)+ self._activity_map[activity_id] = name+ self._activity_type_map[activity_id] = activity++ direction = "up" if is_upload else "down"+ if "source" in text.lower() and not source.startswith("/nix/store/"):+ return f"Copying {direction} {name}"+ return f"Copying {direction} {name}"+ def _should_track_activity( self, name: str, r"connecting to", r"copying \d+ paths?", r"copying path",+ r"copying source", r"this derivation will be built", ] for pattern in status_patterns: clean = line.strip() if clean and len(clean) < 100: self.state.status_message = clean++ # Track aggregate copy progress (e.g., "copying 23 paths to 'ssh://...'")+ paths_match = re.search(r"copying (\d+) paths? to", line.lower())+ if paths_match:+ total_paths = int(paths_match.group(1))+ # Create or update an aggregate upload tracker+ agg_name = f"upload ({total_paths} paths)"+ if agg_name not in self.state.dependencies:+ dep = Dependency(+ name=agg_name,+ status=BuildStatus.RUNNING,+ activity_type=ActivityType.UPLOAD,+ started_at=datetime.now(),+ size=total_paths, # Use size field to track total paths+ downloaded=0, # Use downloaded to track completed paths+ )+ self.state.add_dependency(dep)+ self.state.running_uploads.add(agg_name) break++ # Track individual path completion during aggregate copy+ # Nix outputs: "copied 1 path" or "copied 5/23 paths"+ copied_match = re.search(r"copied (\d+)(?:/(\d+))? paths?", line.lower())+ if copied_match:+ completed = int(copied_match.group(1))+ total = int(copied_match.group(2)) if copied_match.group(2) else None+ # Update aggregate upload tracker (named "upload (N paths)")+ for name in list(self.state.running_uploads):+ if 'paths' in name:+ dep = self.state.dependencies.get(name)+ if dep and dep.activity_type == ActivityType.UPLOAD:+ dep.downloaded = completed+ if total:+ dep.size = total if self.use_json: result = self._parse_json_line(line) return f"Downloading {name}" + # Check for copying operations (uploads/downloads)+ match = self.COPYING_RE.search(line)+ if match:+ copy_type, path = match.groups()+ # Determine if upload (to remote) or download+ is_upload = "to " in line.lower() or "ssh://" in line.lower() or "uploading" in line.lower()+ + # For source copying, use the source path as name+ if copy_type == "source":+ name = path.split('/')[-1][:40] if '/' in path else path[:40]+ else:+ name = self._extract_name(path)+ + activity = ActivityType.UPLOAD if is_upload else ActivityType.DOWNLOAD+ dep = Dependency(+ name=name,+ out_path=path if path.startswith('/nix/store') else None,+ status=BuildStatus.RUNNING,+ activity_type=activity,+ started_at=datetime.now(),+ )+ self.state.add_dependency(dep)+ + direction = "up" if is_upload else "down"+ return f"Copying {direction} {name}"+ # Check for built outputs match = self.BUILT_RE.match(line) if match: text = data.get("text", "") fields = data.get("fields", []) level = data.get("level", 4)++ if (+ activity_type >= 100+ and len(fields) >= 2+ and isinstance(fields[0], str)+ and isinstance(fields[1], str)+ ):+ tracked = self._track_transfer_start(activity_id, fields[0], fields[1], text)+ if tracked:+ return tracked # Track activity type for file transfers if activity_type >= 100: return None elif "copying" in text.lower():+ # Try to extract path from text first match = re.search(r"'(/nix/store/[^']+)'", text) if match: path = match.group(1) name = self._extract_name(path)- + # Check if upload or download is_upload = "to " in text.lower() or "uploading" in text.lower()- + activity = ActivityType.UPLOAD if is_upload else ActivityType.DOWNLOAD if self._should_track_activity(name, activity, path): dep = Dependency( ) self.state.add_dependency(dep) self._activity_map[activity_id] = name+ self._activity_type_map[activity_id] = activity direction = "up" if is_upload else "down" return f"Copying {direction} {name}" return None++ # Handle "copying source" with paths in fields+ elif "source" in text.lower() and fields and len(fields) >= 2:+ return self._track_transfer_start(activity_id, fields[0], fields[1], text) # Track evaluation activities if "evaluating" in text.lower(): if name in self.state.dependencies: dep = self.state.dependencies[name]- - # Check for errors in the activity- has_error = False- status = BuildStatus.FAILED if has_error else BuildStatus.DONE- ++ status = dep.status if dep.status == BuildStatus.FAILED else BuildStatus.DONE self.state.update_status(name, status, datetime.now()) # Record build time return None # Type 106 = file transfer progress- # fields[0] = status (100=started, 101=progress, etc)+ # fields[0] = transfer status code # fields[1] = bytes transferred+ # fields[2] may contain total size+ if result_type != 106:+ return None if activity_id not in self._activity_map: return None if name in self.state.dependencies: dep = self.state.dependencies[name] - # Update download progress- if dep.activity_type == ActivityType.DOWNLOAD and len(fields) >= 2:- status_code = fields[0]- bytes_transferred = fields[1] if len(fields) > 1 else 0- - if status_code == 101: # Progress- dep.downloaded = bytes_transferred- # We don't know total size, so can't compute progress- elif status_code == 100: # Done+ # Update download/upload progress+ if dep.activity_type in (ActivityType.DOWNLOAD, ActivityType.UPLOAD) and len(fields) >= 2:+ bytes_transferred = self._coerce_transfer_int(fields[1])+ total_size = self._coerce_transfer_int(fields[2]) if len(fields) > 2 else None++ if bytes_transferred is not None: dep.downloaded = bytes_transferred+ if total_size is not None:+ dep.size = total_size return None # Only set error if it's a real error message if level <= 0 and msg and ("error:" in msg.lower() or "failed" in msg.lower()): self.state.error = msg+ self._mark_failed_builds(msg) return f"ERROR: {msg}" # Level 3 = info about paths