woolly 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- woolly/__init__.py +0 -0
- woolly/__main__.py +482 -0
- woolly-0.1.0.dist-info/METADATA +101 -0
- woolly-0.1.0.dist-info/RECORD +7 -0
- woolly-0.1.0.dist-info/WHEEL +4 -0
- woolly-0.1.0.dist-info/entry_points.txt +2 -0
- woolly-0.1.0.dist-info/licenses/LICENSE +19 -0
woolly/__init__.py
ADDED
|
File without changes
|
woolly/__main__.py
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import requests
|
|
5
|
+
import subprocess
|
|
6
|
+
import re
|
|
7
|
+
import json
|
|
8
|
+
import hashlib
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from rich.tree import Tree
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn, TaskID
|
|
14
|
+
from rich import box
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
CRATES_API = "https://crates.io/api/v1/crates"
|
|
18
|
+
CACHE_DIR = Path.home() / ".cache" / "fedora-rust-checker"
|
|
19
|
+
CACHE_TTL = 86400 * 7 # 7 days for crates.io data
|
|
20
|
+
FEDORA_CACHE_TTL = 86400 # 1 day for Fedora repoquery data
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
# Required headers for crates.io API
|
|
25
|
+
HEADERS = {"User-Agent": "fedora-rust-checker@0.1.0"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ------------------------------------------------------------
|
|
29
|
+
# Disk Cache Helpers
|
|
30
|
+
# ------------------------------------------------------------
|
|
31
|
+
def ensure_cache_dir():
|
|
32
|
+
"""Create cache directory if it doesn't exist."""
|
|
33
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
(CACHE_DIR / "crates").mkdir(exist_ok=True)
|
|
35
|
+
(CACHE_DIR / "fedora").mkdir(exist_ok=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_cache_path(namespace: str, key: str) -> Path:
|
|
39
|
+
"""Get path for a cache entry."""
|
|
40
|
+
safe_key = hashlib.md5(key.encode()).hexdigest()
|
|
41
|
+
return CACHE_DIR / namespace / f"{safe_key}.json"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_cache(namespace: str, key: str, ttl: int = CACHE_TTL):
|
|
45
|
+
"""Read from disk cache if not expired."""
|
|
46
|
+
path = get_cache_path(namespace, key)
|
|
47
|
+
if not path.exists():
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(path.read_text())
|
|
52
|
+
if time.time() - data.get("timestamp", 0) > ttl:
|
|
53
|
+
return None # Expired
|
|
54
|
+
return data.get("value")
|
|
55
|
+
except (json.JSONDecodeError, KeyError):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write_cache(namespace: str, key: str, value):
|
|
60
|
+
"""Write to disk cache."""
|
|
61
|
+
ensure_cache_dir()
|
|
62
|
+
path = get_cache_path(namespace, key)
|
|
63
|
+
data = {"timestamp": time.time(), "value": value}
|
|
64
|
+
path.write_text(json.dumps(data))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------
|
|
68
|
+
# Fedora repoquery helpers
|
|
69
|
+
# ------------------------------------------------------------
|
|
70
|
+
def repoquery_crate(crate: str):
|
|
71
|
+
"""
|
|
72
|
+
Query Fedora for a crate using the virtual provides format: crate(name)
|
|
73
|
+
Returns (is_packaged, versions_list, package_names)
|
|
74
|
+
"""
|
|
75
|
+
cache_key = f"repoquery:{crate}"
|
|
76
|
+
cached = read_cache("fedora", cache_key, FEDORA_CACHE_TTL)
|
|
77
|
+
if cached is not None:
|
|
78
|
+
return tuple(cached)
|
|
79
|
+
|
|
80
|
+
provide_pattern = f"crate({crate})"
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
out = subprocess.check_output(
|
|
84
|
+
["dnf", "repoquery", "--whatprovides", provide_pattern,
|
|
85
|
+
"--queryformat", "%{NAME}|%{VERSION}"],
|
|
86
|
+
stderr=subprocess.DEVNULL,
|
|
87
|
+
).decode().strip()
|
|
88
|
+
|
|
89
|
+
if not out:
|
|
90
|
+
result = (False, [], [])
|
|
91
|
+
write_cache("fedora", cache_key, list(result))
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
versions = set()
|
|
95
|
+
packages = set()
|
|
96
|
+
for line in out.split("\n"):
|
|
97
|
+
if "|" in line:
|
|
98
|
+
pkg, ver = line.split("|", 1)
|
|
99
|
+
packages.add(pkg)
|
|
100
|
+
versions.add(ver)
|
|
101
|
+
|
|
102
|
+
result = (True, sorted(versions), sorted(packages))
|
|
103
|
+
write_cache("fedora", cache_key, [result[0], result[1], result[2]])
|
|
104
|
+
return result
|
|
105
|
+
except subprocess.CalledProcessError:
|
|
106
|
+
result = (False, [], [])
|
|
107
|
+
write_cache("fedora", cache_key, list(result))
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_crate_provides_version(crate: str):
|
|
112
|
+
"""
|
|
113
|
+
Get the actual crate version provided by Fedora packages.
|
|
114
|
+
"""
|
|
115
|
+
cache_key = f"provides:{crate}"
|
|
116
|
+
cached = read_cache("fedora", cache_key, FEDORA_CACHE_TTL)
|
|
117
|
+
if cached is not None:
|
|
118
|
+
return cached
|
|
119
|
+
|
|
120
|
+
provide_pattern = f"crate({crate})"
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
out = subprocess.check_output(
|
|
124
|
+
["dnf", "repoquery", "--provides", "--whatprovides", provide_pattern],
|
|
125
|
+
stderr=subprocess.DEVNULL,
|
|
126
|
+
).decode().strip()
|
|
127
|
+
|
|
128
|
+
if not out:
|
|
129
|
+
write_cache("fedora", cache_key, [])
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
versions = set()
|
|
133
|
+
pattern = re.compile(rf"crate\({re.escape(crate)}\)\s*=\s*([\d.]+)")
|
|
134
|
+
for line in out.split("\n"):
|
|
135
|
+
match = pattern.search(line)
|
|
136
|
+
if match:
|
|
137
|
+
versions.add(match.group(1))
|
|
138
|
+
|
|
139
|
+
result = sorted(versions)
|
|
140
|
+
write_cache("fedora", cache_key, result)
|
|
141
|
+
return result
|
|
142
|
+
except subprocess.CalledProcessError:
|
|
143
|
+
write_cache("fedora", cache_key, [])
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def crate_is_packaged(crate: str):
|
|
148
|
+
"""
|
|
149
|
+
Check if a crate is packaged in Fedora.
|
|
150
|
+
Returns (is_packaged, crate_versions, package_names)
|
|
151
|
+
"""
|
|
152
|
+
is_packaged, pkg_versions, packages = repoquery_crate(crate)
|
|
153
|
+
|
|
154
|
+
if not is_packaged:
|
|
155
|
+
alt_name = crate.replace("-", "_")
|
|
156
|
+
if alt_name != crate:
|
|
157
|
+
is_packaged, pkg_versions, packages = repoquery_crate(alt_name)
|
|
158
|
+
|
|
159
|
+
if not is_packaged:
|
|
160
|
+
alt_name = crate.replace("_", "-")
|
|
161
|
+
if alt_name != crate:
|
|
162
|
+
is_packaged, pkg_versions, packages = repoquery_crate(alt_name)
|
|
163
|
+
|
|
164
|
+
if is_packaged:
|
|
165
|
+
crate_versions = get_crate_provides_version(crate)
|
|
166
|
+
if not crate_versions:
|
|
167
|
+
crate_versions = pkg_versions
|
|
168
|
+
return (True, crate_versions, packages)
|
|
169
|
+
|
|
170
|
+
return (False, [], [])
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------
|
|
174
|
+
# Crates.io API helpers
|
|
175
|
+
# ------------------------------------------------------------
|
|
176
|
+
def fetch_crate_info(crate_name: str):
|
|
177
|
+
"""Fetch basic crate info (latest version, etc.)"""
|
|
178
|
+
cache_key = f"info:{crate_name}"
|
|
179
|
+
cached = read_cache("crates", cache_key)
|
|
180
|
+
if cached is not None:
|
|
181
|
+
return cached
|
|
182
|
+
|
|
183
|
+
url = f"{CRATES_API}/{crate_name}"
|
|
184
|
+
r = requests.get(url, headers=HEADERS)
|
|
185
|
+
if r.status_code == 404:
|
|
186
|
+
write_cache("crates", cache_key, None)
|
|
187
|
+
return None
|
|
188
|
+
if r.status_code != 200:
|
|
189
|
+
raise RuntimeError(f"Failed to fetch metadata for crate {crate_name}: {r.status_code}")
|
|
190
|
+
|
|
191
|
+
data = r.json()
|
|
192
|
+
write_cache("crates", cache_key, data)
|
|
193
|
+
return data
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def fetch_dependencies(crate_name: str, version: str):
|
|
197
|
+
"""
|
|
198
|
+
Fetch dependencies for a specific crate version.
|
|
199
|
+
"""
|
|
200
|
+
cache_key = f"deps:{crate_name}:{version}"
|
|
201
|
+
cached = read_cache("crates", cache_key)
|
|
202
|
+
if cached is not None:
|
|
203
|
+
return cached
|
|
204
|
+
|
|
205
|
+
url = f"{CRATES_API}/{crate_name}/{version}/dependencies"
|
|
206
|
+
r = requests.get(url, headers=HEADERS)
|
|
207
|
+
if r.status_code != 200:
|
|
208
|
+
write_cache("crates", cache_key, [])
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
data = r.json()
|
|
212
|
+
deps = data.get("dependencies", [])
|
|
213
|
+
write_cache("crates", cache_key, deps)
|
|
214
|
+
return deps
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_latest_version(crate_name: str):
|
|
218
|
+
"""Get the latest (newest) version of a crate."""
|
|
219
|
+
info = fetch_crate_info(crate_name)
|
|
220
|
+
if info is None:
|
|
221
|
+
return None
|
|
222
|
+
return info["crate"]["newest_version"]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_normal_dependencies(crate_name: str, version: Optional[str] = None):
|
|
226
|
+
"""
|
|
227
|
+
Returns normal (non-dev, non-build) dependencies as list of tuples:
|
|
228
|
+
[("dep_name", "version_req"), ...]
|
|
229
|
+
"""
|
|
230
|
+
if version is None:
|
|
231
|
+
version = get_latest_version(crate_name)
|
|
232
|
+
if version is None:
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
deps = fetch_dependencies(crate_name, version)
|
|
236
|
+
result = []
|
|
237
|
+
for d in deps:
|
|
238
|
+
if d.get("kind") == "normal":
|
|
239
|
+
if d.get("optional", False):
|
|
240
|
+
continue
|
|
241
|
+
result.append((d["crate_id"], d["req"]))
|
|
242
|
+
return result
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ------------------------------------------------------------
|
|
246
|
+
# Progress Tracker
|
|
247
|
+
# ------------------------------------------------------------
|
|
248
|
+
class ProgressTracker:
|
|
249
|
+
def __init__(self):
|
|
250
|
+
self.progress = Progress(
|
|
251
|
+
SpinnerColumn(),
|
|
252
|
+
TextColumn("[bold blue]{task.description}"),
|
|
253
|
+
BarColumn(bar_width=30),
|
|
254
|
+
TaskProgressColumn(),
|
|
255
|
+
TextColumn("•"),
|
|
256
|
+
TimeElapsedColumn(),
|
|
257
|
+
TextColumn("[dim]{task.fields[status]}[/dim]"),
|
|
258
|
+
console=console,
|
|
259
|
+
)
|
|
260
|
+
self.task: TaskID = TaskID(0)
|
|
261
|
+
self.processed = 0
|
|
262
|
+
self.total_discovered = 0
|
|
263
|
+
|
|
264
|
+
def start(self):
|
|
265
|
+
self.task = self.progress.add_task(
|
|
266
|
+
"Analyzing dependencies",
|
|
267
|
+
total=None,
|
|
268
|
+
status="starting..."
|
|
269
|
+
)
|
|
270
|
+
self.progress.start()
|
|
271
|
+
|
|
272
|
+
def stop(self):
|
|
273
|
+
self.progress.stop()
|
|
274
|
+
|
|
275
|
+
def update(self, crate_name: str, discovered: int = 0):
|
|
276
|
+
self.processed += 1
|
|
277
|
+
self.total_discovered += discovered
|
|
278
|
+
|
|
279
|
+
if self.total_discovered > 0:
|
|
280
|
+
self.progress.update(
|
|
281
|
+
self.task,
|
|
282
|
+
completed=self.processed,
|
|
283
|
+
total=self.processed + self.total_discovered,
|
|
284
|
+
status=f"checking: {crate_name}"
|
|
285
|
+
)
|
|
286
|
+
else:
|
|
287
|
+
self.progress.update(
|
|
288
|
+
self.task,
|
|
289
|
+
status=f"checking: {crate_name}"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def finish(self):
|
|
293
|
+
self.progress.update(
|
|
294
|
+
self.task,
|
|
295
|
+
completed=self.processed,
|
|
296
|
+
total=self.processed,
|
|
297
|
+
status="[green]complete![/green]"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ------------------------------------------------------------
|
|
302
|
+
# Tree Builder (FULLY RECURSIVE)
|
|
303
|
+
# ------------------------------------------------------------
|
|
304
|
+
def build_tree(crate_name: str, version: Optional[str] = None, visited=None, depth=0, max_depth=50, tracker=None):
|
|
305
|
+
if visited is None:
|
|
306
|
+
visited = set()
|
|
307
|
+
|
|
308
|
+
if depth > max_depth:
|
|
309
|
+
return f"[dim]{crate_name} (max depth reached)[/dim]"
|
|
310
|
+
|
|
311
|
+
if crate_name in visited:
|
|
312
|
+
return f"[dim]{crate_name} (already visited)[/dim]"
|
|
313
|
+
visited.add(crate_name)
|
|
314
|
+
|
|
315
|
+
if tracker:
|
|
316
|
+
tracker.update(crate_name)
|
|
317
|
+
|
|
318
|
+
if version is None:
|
|
319
|
+
version = get_latest_version(crate_name)
|
|
320
|
+
if version is None:
|
|
321
|
+
return f"[bold red]{crate_name}[/bold red] • [red]not found on crates.io[/red]"
|
|
322
|
+
|
|
323
|
+
# Check Fedora packaging status
|
|
324
|
+
is_packaged, fedora_versions, packages = crate_is_packaged(crate_name)
|
|
325
|
+
|
|
326
|
+
if is_packaged:
|
|
327
|
+
ver_str = ", ".join(fedora_versions) if fedora_versions else "unknown"
|
|
328
|
+
pkg_str = ", ".join(packages) if packages else ""
|
|
329
|
+
label = (f"[bold]{crate_name}[/bold] [dim]v{version}[/dim] • "
|
|
330
|
+
f"[green]✓ packaged[/green] [dim]({ver_str})[/dim]")
|
|
331
|
+
if pkg_str:
|
|
332
|
+
label += f" [dim cyan][{pkg_str}][/dim cyan]"
|
|
333
|
+
else:
|
|
334
|
+
label = f"[bold]{crate_name}[/bold] [dim]v{version}[/dim] • [red]✗ not packaged[/red]"
|
|
335
|
+
|
|
336
|
+
node = Tree(label)
|
|
337
|
+
|
|
338
|
+
# ALWAYS recurse into dependencies regardless of packaging status
|
|
339
|
+
deps = get_normal_dependencies(crate_name, version)
|
|
340
|
+
if tracker and deps:
|
|
341
|
+
tracker.update(crate_name, discovered=len(deps))
|
|
342
|
+
|
|
343
|
+
for (dep_name, dep_req) in deps:
|
|
344
|
+
child = build_tree(dep_name, None, visited, depth + 1, max_depth, tracker)
|
|
345
|
+
if isinstance(child, str):
|
|
346
|
+
node.add(child)
|
|
347
|
+
else:
|
|
348
|
+
node.add(child)
|
|
349
|
+
|
|
350
|
+
return node
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ------------------------------------------------------------
|
|
354
|
+
# Summary Collection
|
|
355
|
+
# ------------------------------------------------------------
|
|
356
|
+
def collect_stats(tree, stats=None):
|
|
357
|
+
"""Walk the tree and collect statistics."""
|
|
358
|
+
if stats is None:
|
|
359
|
+
stats = {"total": 0, "packaged": 0, "missing": 0, "missing_list": [], "packaged_list": []}
|
|
360
|
+
|
|
361
|
+
def walk(t):
|
|
362
|
+
if isinstance(t, str):
|
|
363
|
+
stats["total"] += 1
|
|
364
|
+
if "not packaged" in t or "not found" in t:
|
|
365
|
+
stats["missing"] += 1
|
|
366
|
+
name = t.split("[/bold]")[0].split("]")[-1] if "[/bold]" in t else t.split()[0]
|
|
367
|
+
stats["missing_list"].append(name)
|
|
368
|
+
elif "packaged" in t:
|
|
369
|
+
stats["packaged"] += 1
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
if hasattr(t, "label"):
|
|
373
|
+
label = str(t.label)
|
|
374
|
+
stats["total"] += 1
|
|
375
|
+
if "not packaged" in label or "not found" in label:
|
|
376
|
+
stats["missing"] += 1
|
|
377
|
+
name = label.split("[/bold]")[0].split("[bold]")[-1] if "[bold]" in label else "unknown"
|
|
378
|
+
stats["missing_list"].append(name)
|
|
379
|
+
elif "packaged" in label:
|
|
380
|
+
stats["packaged"] += 1
|
|
381
|
+
name = label.split("[/bold]")[0].split("[bold]")[-1] if "[bold]" in label else "unknown"
|
|
382
|
+
stats["packaged_list"].append(name)
|
|
383
|
+
|
|
384
|
+
if hasattr(t, "children"):
|
|
385
|
+
for child in t.children:
|
|
386
|
+
walk(child)
|
|
387
|
+
|
|
388
|
+
walk(tree)
|
|
389
|
+
return stats
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def print_summary_table(root_crate, tree):
|
|
393
|
+
stats = collect_stats(tree)
|
|
394
|
+
|
|
395
|
+
table = Table(title=f"Dependency Summary for '{root_crate}'", box=box.ROUNDED)
|
|
396
|
+
table.add_column("Metric", style="bold")
|
|
397
|
+
table.add_column("Value", justify="right")
|
|
398
|
+
|
|
399
|
+
table.add_row("Total dependencies checked", str(stats["total"]))
|
|
400
|
+
table.add_row("[green]Packaged in Fedora[/green]", str(stats["packaged"]))
|
|
401
|
+
table.add_row("[red]Missing from Fedora[/red]", str(stats["missing"]))
|
|
402
|
+
|
|
403
|
+
console.print(table)
|
|
404
|
+
console.print()
|
|
405
|
+
|
|
406
|
+
if stats["missing_list"]:
|
|
407
|
+
console.print("[bold]Missing crates that need packaging:[/bold]")
|
|
408
|
+
for name in sorted(set(stats["missing_list"])):
|
|
409
|
+
console.print(f" • {name}")
|
|
410
|
+
console.print()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def clear_cache(namespace: Optional[str] = None):
|
|
414
|
+
"""Clear disk cache."""
|
|
415
|
+
if namespace:
|
|
416
|
+
cache_path = CACHE_DIR / namespace
|
|
417
|
+
if cache_path.exists():
|
|
418
|
+
for f in cache_path.glob("*.json"):
|
|
419
|
+
f.unlink()
|
|
420
|
+
console.print(f"[yellow]Cleared {namespace} cache[/yellow]")
|
|
421
|
+
else:
|
|
422
|
+
for ns in ["crates", "fedora"]:
|
|
423
|
+
cache_path = CACHE_DIR / ns
|
|
424
|
+
if cache_path.exists():
|
|
425
|
+
for f in cache_path.glob("*.json"):
|
|
426
|
+
f.unlink()
|
|
427
|
+
console.print("[yellow]Cleared all caches[/yellow]")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def main() -> int:
|
|
431
|
+
parser = argparse.ArgumentParser(
|
|
432
|
+
description="Check if a Rust crate's dependencies are packaged in Fedora"
|
|
433
|
+
)
|
|
434
|
+
parser.add_argument("crate", nargs="?", help="Crate name on crates.io (e.g., ripgrep)")
|
|
435
|
+
parser.add_argument("--version", "-v", help="Specific version to check")
|
|
436
|
+
parser.add_argument("--max-depth", "-d", type=int, default=50, help="Maximum recursion depth")
|
|
437
|
+
parser.add_argument("--no-progress", action="store_true", help="Disable progress bar")
|
|
438
|
+
parser.add_argument("--clear-cache", action="store_true", help="Clear all cached data")
|
|
439
|
+
parser.add_argument("--clear-fedora-cache", action="store_true", help="Clear Fedora repoquery cache")
|
|
440
|
+
args = parser.parse_args()
|
|
441
|
+
|
|
442
|
+
if args.clear_cache:
|
|
443
|
+
clear_cache()
|
|
444
|
+
if not args.crate:
|
|
445
|
+
exit(0)
|
|
446
|
+
|
|
447
|
+
if args.clear_fedora_cache:
|
|
448
|
+
clear_cache("fedora")
|
|
449
|
+
if not args.crate:
|
|
450
|
+
exit(0)
|
|
451
|
+
|
|
452
|
+
if not args.crate:
|
|
453
|
+
parser.print_help()
|
|
454
|
+
exit(1)
|
|
455
|
+
|
|
456
|
+
console.print(f"\n[bold underline]Analyzing crate:[/] {args.crate}")
|
|
457
|
+
console.print(f"[dim]Cache directory: {CACHE_DIR}[/dim]\n")
|
|
458
|
+
|
|
459
|
+
tracker = None if args.no_progress else ProgressTracker()
|
|
460
|
+
|
|
461
|
+
if tracker:
|
|
462
|
+
tracker.start()
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
tree = build_tree(args.crate, args.version, max_depth=args.max_depth, tracker=tracker)
|
|
466
|
+
if tracker:
|
|
467
|
+
tracker.finish()
|
|
468
|
+
finally:
|
|
469
|
+
if tracker:
|
|
470
|
+
tracker.stop()
|
|
471
|
+
|
|
472
|
+
console.print()
|
|
473
|
+
print_summary_table(args.crate, tree)
|
|
474
|
+
|
|
475
|
+
console.print("[bold]Dependency Tree:[/bold]")
|
|
476
|
+
console.print(tree)
|
|
477
|
+
console.print()
|
|
478
|
+
|
|
479
|
+
return 0
|
|
480
|
+
|
|
481
|
+
if __name__ == "__main__":
|
|
482
|
+
sys.exit(main())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: woolly
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Recursively search for RPMs in Fedora for Rust crates.
|
|
5
|
+
Author-email: Rodolfo Olivieri <rodolfo.olivieri3@gmail.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: requests>=2.32.5
|
|
26
|
+
Requires-Dist: rich>=14.2.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Woolly
|
|
30
|
+
|
|
31
|
+
Recursively search for RPMs in Fedora for a given Rust [crate](https://crate.io).
|
|
32
|
+
|
|
33
|
+
> This tool is merely a starting point for figuring out how much packaging
|
|
34
|
+
> effort you will need to bring a rust crate over to Fedora.
|
|
35
|
+
|
|
36
|
+
## What does "woolly" means?
|
|
37
|
+
|
|
38
|
+
Nothing. I just liked the name.
|
|
39
|
+
|
|
40
|
+
## Running the project
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
$ uv run rust_rpm_inspector
|
|
44
|
+
|
|
45
|
+
Analyzing crate: cliclack
|
|
46
|
+
Cache directory: /home/r0x0d/.cache/fedora-rust-checker
|
|
47
|
+
|
|
48
|
+
Analyzing dependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% • 0:00:15 complete!
|
|
49
|
+
|
|
50
|
+
Dependency Summary for 'cliclack'
|
|
51
|
+
╭────────────────────────────┬───────╮
|
|
52
|
+
│ Metric │ Value │
|
|
53
|
+
├────────────────────────────┼───────┤
|
|
54
|
+
│ Total dependencies checked │ 7 │
|
|
55
|
+
│ Packaged in Fedora │ 0 │
|
|
56
|
+
│ Missing from Fedora │ 1 │
|
|
57
|
+
╰────────────────────────────┴───────╯
|
|
58
|
+
|
|
59
|
+
Missing crates that need packaging:
|
|
60
|
+
• cliclack
|
|
61
|
+
|
|
62
|
+
Dependency Tree:
|
|
63
|
+
cliclack v0.3.6 • ✗ not packaged
|
|
64
|
+
├── console v0.16.1 • ✓ packaged (0.16.1)
|
|
65
|
+
│ ├── encode_unicode v1.0.0 • ✓ packaged (1.0.0)
|
|
66
|
+
│ └── windows-sys v0.61.2 • ✗ not packaged
|
|
67
|
+
│ └── windows-link v0.2.1 • ✗ not packaged
|
|
68
|
+
├── indicatif v0.18.3 • ✓ packaged (0.16.2, 0.18.0, 0.18.1)
|
|
69
|
+
│ ├── console (already visited)
|
|
70
|
+
│ ├── portable-atomic v1.11.1 • ✓ packaged (1.11.1)
|
|
71
|
+
│ ├── unit-prefix v0.5.2 • ✓ packaged (0.5.1)
|
|
72
|
+
│ └── web-time v1.1.0 • ✓ packaged (1.1.0)
|
|
73
|
+
│ ├── js-sys v0.3.82 • ✗ not packaged
|
|
74
|
+
│ │ ├── once_cell v1.21.3 • ✓ packaged (1.21.3)
|
|
75
|
+
│ │ └── wasm-bindgen v0.2.105 • ✗ not packaged
|
|
76
|
+
│ │ ├── cfg-if v1.0.4 • ✓ packaged (0.1.10, 1.0.3, 1.0.4)
|
|
77
|
+
│ │ ├── once_cell (already visited)
|
|
78
|
+
│ │ ├── wasm-bindgen-macro v0.2.105 • ✗ not packaged
|
|
79
|
+
│ │ │ ├── quote v1.0.42 • ✓ packaged (0.3.15, 1.0.40, 1.0.41)
|
|
80
|
+
│ │ │ │ └── proc-macro2 v1.0.103 • ✓ packaged (1.0.101, 1.0.103)
|
|
81
|
+
│ │ │ │ └── unicode-ident v1.0.22 • ✓ packaged (1.0.19, 1.0.22)
|
|
82
|
+
│ │ │ └── wasm-bindgen-macro-support v0.2.105 • ✗ not packaged
|
|
83
|
+
│ │ │ ├── bumpalo v3.19.0 • ✓ packaged (3.19.0)
|
|
84
|
+
│ │ │ ├── proc-macro2 (already visited)
|
|
85
|
+
│ │ │ ├── quote (already visited)
|
|
86
|
+
│ │ │ ├── syn v2.0.110 • ✓ packaged (1.0.109, 2.0.106, 2.0.108)
|
|
87
|
+
│ │ │ │ ├── proc-macro2 (already visited)
|
|
88
|
+
│ │ │ │ └── unicode-ident (already visited)
|
|
89
|
+
│ │ │ └── wasm-bindgen-shared v0.2.105 • ✗ not packaged
|
|
90
|
+
│ │ │ └── unicode-ident (already visited)
|
|
91
|
+
│ │ └── wasm-bindgen-shared (already visited)
|
|
92
|
+
│ └── wasm-bindgen (already visited)
|
|
93
|
+
├── once_cell (already visited)
|
|
94
|
+
├── strsim v0.11.1 • ✓ packaged (0.10.0, 0.11.1)
|
|
95
|
+
├── textwrap v0.16.2 • ✓ packaged (0.11.0, 0.15.2, 0.16.2)
|
|
96
|
+
└── zeroize v1.8.2 • ✓ packaged (1.8.1, 1.8.2)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Keep in mind that you may not need all of RPMs to be present in Fedora, like
|
|
100
|
+
the output above, we have `windows*` crates in the dependency tree, but they
|
|
101
|
+
are not used at all.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
woolly/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
woolly/__main__.py,sha256=_XB9nZAFBJptgQYaL6OO9VRLh2SYGjRTPS5DXndJx7w,15592
|
|
3
|
+
woolly-0.1.0.dist-info/METADATA,sha256=H11_hfzIbdrVvqsHeL4fvLcxgQeOeCrnB-FG8G47i2o,4895
|
|
4
|
+
woolly-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
woolly-0.1.0.dist-info/entry_points.txt,sha256=tMSCR0PXsaIWiiY5fwQ5eowwNqPSrH0j9QriPDoE75k,48
|
|
6
|
+
woolly-0.1.0.dist-info/licenses/LICENSE,sha256=TnfrdmTYydTTenujKk2LdAuDuLSMwjpXhW7EU2VR0e8,1064
|
|
7
|
+
woolly-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright 2025 Rodolfo Olivieri
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the “Software”), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
8
|
+
so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|