lfpga 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.
lfpga/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """lfpga: a package manager for FPGA/HDL development.
2
+
3
+ Resolve, fetch and assemble open FPGA IP cores from the LibFPGA registry
4
+ (https://libfpga.com/cores/) into a source list your simulator or synthesis
5
+ tool can consume. The registry is the pypi; this is the pip.
6
+ """
7
+
8
+ __version__ = "0.1.0"
lfpga/cli.py ADDED
@@ -0,0 +1,194 @@
1
+ """The `lfpga` command-line interface."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import os
6
+ import sys
7
+
8
+ from . import __version__, emit, lockfile
9
+ from .fetch import fetch
10
+ from .manifest import (MANIFEST_NAME, Dependency, Manifest,
11
+ ManifestError)
12
+ from .registry import RegistryClient, RegistryError
13
+ from .resolve import LockedPackage, resolve
14
+
15
+
16
+ def _say(msg=""):
17
+ print(msg)
18
+
19
+
20
+ def _err(msg):
21
+ print(f"lfpga: {msg}", file=sys.stderr)
22
+
23
+
24
+ def _badges(verified):
25
+ earned = [k for k in ("lint", "synth", "testbench", "formal")
26
+ if verified.get(k)]
27
+ return "✓ " + ", ".join(earned) if earned else "unverified"
28
+
29
+
30
+ # --- commands ---------------------------------------------------------------
31
+
32
+ def cmd_init(args):
33
+ if os.path.exists(MANIFEST_NAME) and not args.force:
34
+ _err("libfpga.yaml already exists (use --force to overwrite)")
35
+ return 1
36
+ name = args.name or os.path.basename(os.getcwd())
37
+ m = Manifest(Manifest.scaffold(name))
38
+ m.save()
39
+ _say(f"Created libfpga.yaml for '{name}'.")
40
+ _say("Add cores with: lfpga add <name> (e.g. lfpga add picorv32)")
41
+ return 0
42
+
43
+
44
+ def _parse_pkg_arg(spec):
45
+ """'name', 'name@rev', or a git URL -> (name, Dependency)."""
46
+ if spec.startswith(("http://", "https://", "git@")):
47
+ name = spec.rstrip("/").split("/")[-1].removesuffix(".git")
48
+ return name, Dependency(name, git=spec)
49
+ if "@" in spec:
50
+ name, rev = spec.split("@", 1)
51
+ return name, Dependency(name, rev=rev)
52
+ return spec, Dependency(spec)
53
+
54
+
55
+ def cmd_add(args):
56
+ try:
57
+ m = Manifest.load()
58
+ except ManifestError:
59
+ m = Manifest(Manifest.scaffold(os.path.basename(os.getcwd())))
60
+ reg = RegistryClient(args.registry)
61
+ name, dep = _parse_pkg_arg(args.package)
62
+ if not dep.git:
63
+ try:
64
+ info = reg.package(name)
65
+ except RegistryError as e:
66
+ _err(str(e))
67
+ return 1
68
+ _say(f"Found {info['name']}: {info.get('source_url','')}")
69
+ _say(f" license {info.get('license','?')} · "
70
+ f"{', '.join(info.get('languages', []))} · "
71
+ f"{_badges(info.get('verification', {}))}")
72
+ if info.get("unverified"):
73
+ _say(" note: not verified by the LibFPGA toolchain yet.")
74
+ m.add_dependency(dep)
75
+ m.save()
76
+ _say(f"Added '{name}' to libfpga.yaml. Run `lfpga install`.")
77
+ return 0
78
+
79
+
80
+ def cmd_install(args):
81
+ try:
82
+ m = Manifest.load()
83
+ except ManifestError as e:
84
+ _err(str(e))
85
+ return 1
86
+ if not m.dependencies:
87
+ _say("No dependencies to install. Add some with `lfpga add`.")
88
+ return 0
89
+ reg = RegistryClient(args.registry)
90
+ _say(f"Resolving {len(m.dependencies)} dependencies...")
91
+ try:
92
+ locked = resolve(m, reg)
93
+ except (RegistryError, RuntimeError, OSError) as e:
94
+ _err(str(e))
95
+ return 1
96
+ lockfile.dump(locked)
97
+ out = emit.write_filelist(locked)
98
+ _say("")
99
+ unverified = 0
100
+ for pkg in locked:
101
+ mark = _badges(pkg.verified)
102
+ if not any(pkg.verified.values()):
103
+ unverified += 1
104
+ _say(f" {pkg.name:22} {pkg.rev[:12]} {len(pkg.files)} files {mark}")
105
+ if pkg.note:
106
+ _say(f" {'':22} ({pkg.note})")
107
+ _say("")
108
+ _say(f"Wrote lfpga.lock and {out} "
109
+ f"({sum(len(p.files) for p in locked)} source files).")
110
+ if unverified:
111
+ _say(f"Note: {unverified} package(s) carry no earned verification "
112
+ f"badge. See https://libfpga.com/cores/")
113
+ return 0
114
+
115
+
116
+ def cmd_list(args):
117
+ if not os.path.exists(lockfile.LOCK_NAME):
118
+ _err("no lfpga.lock. Run `lfpga install` first.")
119
+ return 1
120
+ for pkg in lockfile.load():
121
+ _say(f" {pkg.name:22} {pkg.rev[:12]} "
122
+ f"{len(pkg.files)} files {_badges(pkg.verified)}")
123
+ _say(f" {'':22} {pkg.source}")
124
+ return 0
125
+
126
+
127
+ def cmd_sources(args):
128
+ if not os.path.exists(lockfile.LOCK_NAME):
129
+ _err("no lfpga.lock. Run `lfpga install` first.")
130
+ return 1
131
+ locked = lockfile.load()
132
+ # ensure the pinned sources are in the cache, then emit
133
+ for pkg in locked:
134
+ pkg.path, _ = fetch(pkg.name, pkg.source, pkg.rev)
135
+ if args.format == "verilator":
136
+ _say(" ".join(emit.verilator_args(locked)))
137
+ else:
138
+ text = emit.filelist(locked)
139
+ if args.output:
140
+ with open(args.output, "w") as f:
141
+ f.write(text)
142
+ _say(f"Wrote {args.output}")
143
+ else:
144
+ sys.stdout.write(text)
145
+ return 0
146
+
147
+
148
+ # --- entry point ------------------------------------------------------------
149
+
150
+ def build_parser():
151
+ p = argparse.ArgumentParser(
152
+ prog="lfpga",
153
+ description="A package manager for FPGA/HDL development. "
154
+ "Resolve, fetch and assemble IP cores from the LibFPGA "
155
+ "registry (https://libfpga.com/cores/).")
156
+ p.add_argument("--version", action="version",
157
+ version=f"lfpga {__version__}")
158
+ p.add_argument("--registry", default=None,
159
+ help="registry base URL (default: https://libfpga.com)")
160
+ sub = p.add_subparsers(dest="command", required=True)
161
+
162
+ q = sub.add_parser("init", help="create a libfpga.yaml here")
163
+ q.add_argument("name", nargs="?", help="project name")
164
+ q.add_argument("--force", action="store_true")
165
+ q.set_defaults(func=cmd_init)
166
+
167
+ q = sub.add_parser("add", help="add a dependency")
168
+ q.add_argument("package", help="name, name@rev, or a git URL")
169
+ q.set_defaults(func=cmd_add)
170
+
171
+ q = sub.add_parser("install", help="resolve, lock, fetch and assemble")
172
+ q.set_defaults(func=cmd_install)
173
+
174
+ q = sub.add_parser("list", help="show locked dependencies")
175
+ q.set_defaults(func=cmd_list)
176
+
177
+ q = sub.add_parser("sources", help="emit the assembled source list")
178
+ q.add_argument("--format", choices=["filelist", "verilator"],
179
+ default="filelist")
180
+ q.add_argument("-o", "--output", help="write to a file instead of stdout")
181
+ q.set_defaults(func=cmd_sources)
182
+ return p
183
+
184
+
185
+ def main(argv=None):
186
+ args = build_parser().parse_args(argv)
187
+ try:
188
+ return args.func(args)
189
+ except KeyboardInterrupt:
190
+ return 130
191
+
192
+
193
+ if __name__ == "__main__":
194
+ sys.exit(main())
lfpga/emit.py ADDED
@@ -0,0 +1,46 @@
1
+ """Assemble resolved packages into inputs real EDA tools consume.
2
+
3
+ Phase 0 emits a plain filelist (`build/sources.f`): `+incdir+` lines and
4
+ absolute paths to the cached sources, in dependency order. Verilator,
5
+ Icarus, Vivado (`-f`), Quartus and most tools read this format. Richer
6
+ emitters (FuseSoC .core, Edalize targets) are Phase 2.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+
12
+
13
+ def _abs(pkg, rel):
14
+ return os.path.normpath(os.path.join(pkg.path, rel))
15
+
16
+
17
+ def filelist(locked):
18
+ """Return the text of a `-f` filelist for the resolved packages."""
19
+ lines = []
20
+ for pkg in locked:
21
+ lines.append(f"// --- {pkg.name} @ {pkg.rev[:12]} ---")
22
+ for d in pkg.include_dirs:
23
+ lines.append("+incdir+" + _abs(pkg, d))
24
+ for f in pkg.files:
25
+ lines.append(_abs(pkg, f))
26
+ lines.append("")
27
+ return "\n".join(lines).rstrip() + "\n"
28
+
29
+
30
+ def verilator_args(locked):
31
+ """Return Verilator/Icarus-style argument tokens for the sources."""
32
+ args = []
33
+ for pkg in locked:
34
+ for d in pkg.include_dirs:
35
+ args.append("+incdir+" + _abs(pkg, d))
36
+ for pkg in locked:
37
+ for f in pkg.files:
38
+ args.append(_abs(pkg, f))
39
+ return args
40
+
41
+
42
+ def write_filelist(locked, out_path="build/sources.f"):
43
+ os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
44
+ with open(out_path, "w") as f:
45
+ f.write(filelist(locked))
46
+ return out_path
lfpga/fetch.py ADDED
@@ -0,0 +1,77 @@
1
+ """Fetch package sources with git, into a content cache.
2
+
3
+ FPGA "packages" are source, not binaries: we clone the repo (at a tag,
4
+ branch or commit) and vendor the declared files. Everything is cached
5
+ under ~/.lfpga/cache so repeated installs are fast and offline-friendly.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import subprocess
12
+
13
+ CACHE_ROOT = os.environ.get(
14
+ "LFPGA_CACHE", os.path.join(os.path.expanduser("~"), ".lfpga", "cache"))
15
+
16
+ _SHA = re.compile(r"^[0-9a-f]{7,40}$")
17
+
18
+
19
+ class FetchError(Exception):
20
+ pass
21
+
22
+
23
+ def _run(args, **kw):
24
+ return subprocess.run(args, capture_output=True, text=True, **kw)
25
+
26
+
27
+ def _git(path, *args):
28
+ return _run(["git", "-C", path, *args])
29
+
30
+
31
+ def fetch(name, url, rev=None):
32
+ """Clone `url` (at `rev`, or the default branch HEAD) into the cache.
33
+
34
+ Returns (path, commit_sha). `rev` may be a tag, branch or commit sha.
35
+ """
36
+ os.makedirs(CACHE_ROOT, exist_ok=True)
37
+ dest = os.path.join(CACHE_ROOT, name)
38
+
39
+ if os.path.isdir(os.path.join(dest, ".git")):
40
+ # refresh an existing checkout
41
+ _git(dest, "fetch", "--quiet", "--tags", "origin")
42
+ if rev:
43
+ co = _git(dest, "checkout", "--quiet", rev)
44
+ if co.returncode != 0:
45
+ _reclone(dest, url, rev)
46
+ else:
47
+ _clone(dest, url, rev)
48
+
49
+ head = _git(dest, "rev-parse", "HEAD")
50
+ if head.returncode != 0:
51
+ raise FetchError(f"{name}: could not resolve HEAD after fetch")
52
+ return dest, head.stdout.strip()
53
+
54
+
55
+ def _clone(dest, url, rev):
56
+ # Fast path: shallow clone of a tag/branch (or default HEAD).
57
+ args = ["git", "clone", "--quiet", "--depth", "1"]
58
+ if rev and not _SHA.match(rev):
59
+ args += ["--branch", rev]
60
+ args += [url, dest]
61
+ r = _run(args)
62
+ if r.returncode == 0:
63
+ return
64
+ # rev is a commit sha (or the shallow branch clone failed): full clone.
65
+ _reclone(dest, url, rev)
66
+
67
+
68
+ def _reclone(dest, url, rev):
69
+ import shutil
70
+ shutil.rmtree(dest, ignore_errors=True)
71
+ r = _run(["git", "clone", "--quiet", url, dest])
72
+ if r.returncode != 0:
73
+ raise FetchError(f"clone failed for {url}: {r.stderr.strip()[:200]}")
74
+ if rev:
75
+ co = _git(dest, "checkout", "--quiet", rev)
76
+ if co.returncode != 0:
77
+ raise FetchError(f"rev '{rev}' not found in {url}")
lfpga/lockfile.py ADDED
@@ -0,0 +1,57 @@
1
+ """Read and write lfpga.lock (TOML).
2
+
3
+ Because HDL builds are source-and-elaborate with no ABI, reproducibility is
4
+ lockfile-first: the lock pins the exact commit and the exact build sources
5
+ for every package, so a build is identical across machines and over time.
6
+ Commit it to your repo. Reading uses the stdlib tomllib (Python 3.11+); the
7
+ writer is a small, purpose-built emitter for this fixed schema.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import tomllib
12
+
13
+ from .resolve import LockedPackage
14
+
15
+ LOCK_NAME = "lfpga.lock"
16
+ LOCK_VERSION = 1
17
+
18
+
19
+ def _toml_str(s):
20
+ return '"' + str(s).replace("\\", "\\\\").replace('"', '\\"') + '"'
21
+
22
+
23
+ def _toml_list(items):
24
+ return "[" + ", ".join(_toml_str(i) for i in items) + "]"
25
+
26
+
27
+ def dump(locked, path=LOCK_NAME):
28
+ lines = ["# Auto-generated by lfpga. Commit this file.",
29
+ f"version = {LOCK_VERSION}", ""]
30
+ for pkg in sorted(locked, key=lambda p: p.name):
31
+ lines.append("[[package]]")
32
+ lines.append(f"name = {_toml_str(pkg.name)}")
33
+ lines.append(f"source = {_toml_str(pkg.source)}")
34
+ lines.append(f"rev = {_toml_str(pkg.rev)}")
35
+ lines.append(f"files = {_toml_list(pkg.files)}")
36
+ lines.append(f"include_dirs = {_toml_list(pkg.include_dirs)}")
37
+ if pkg.verified:
38
+ earned = sorted(k for k, v in pkg.verified.items() if v)
39
+ lines.append(f"verified = {_toml_list(earned)}")
40
+ if pkg.note:
41
+ lines.append(f"note = {_toml_str(pkg.note)}")
42
+ lines.append("")
43
+ with open(path, "w") as f:
44
+ f.write("\n".join(lines))
45
+
46
+
47
+ def load(path=LOCK_NAME):
48
+ with open(path, "rb") as f:
49
+ data = tomllib.load(f)
50
+ out = []
51
+ for p in data.get("package", []):
52
+ pkg = LockedPackage(
53
+ p["name"], p["source"], p["rev"],
54
+ p.get("files", []), p.get("include_dirs", []),
55
+ {k: True for k in p.get("verified", [])}, p.get("note", ""))
56
+ out.append(pkg)
57
+ return out
lfpga/manifest.py ADDED
@@ -0,0 +1,106 @@
1
+ """The project manifest: libfpga.yaml.
2
+
3
+ Like Cargo.toml, one file both publishes this repo as a package (the fields
4
+ the LibFPGA registry already understands) and declares its dependencies. A
5
+ leaf design that publishes nothing just uses the `dependencies` section.
6
+
7
+ name: my-soc
8
+ top_module: soc_top
9
+ dependencies:
10
+ libfpga: "*" # latest default branch
11
+ picorv32: { rev: v1.0.3 } # pin a tag/branch/commit
12
+ libfpga-myhdl: { modules: [lfpga_mac] } # sub-select from a repo
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import os
17
+
18
+ import yaml
19
+
20
+ MANIFEST_NAME = "libfpga.yaml"
21
+
22
+
23
+ class ManifestError(Exception):
24
+ pass
25
+
26
+
27
+ class Dependency:
28
+ """One dependency line. `spec` is a version string or a table."""
29
+
30
+ def __init__(self, name, version="*", rev=None, git=None, modules=None):
31
+ self.name = name
32
+ self.version = version or "*"
33
+ self.rev = rev
34
+ self.git = git
35
+ self.modules = list(modules or [])
36
+
37
+ @classmethod
38
+ def from_spec(cls, name, spec):
39
+ if spec is None or isinstance(spec, str):
40
+ return cls(name, version=spec or "*")
41
+ if not isinstance(spec, dict):
42
+ raise ManifestError(f"bad dependency spec for '{name}': {spec!r}")
43
+ return cls(name, version=spec.get("version", "*"),
44
+ rev=spec.get("rev"), git=spec.get("git"),
45
+ modules=spec.get("modules"))
46
+
47
+ def to_spec(self):
48
+ """Round-trip back to the compact YAML form."""
49
+ if not self.rev and not self.git and not self.modules \
50
+ and self.version in ("*", None):
51
+ return "*"
52
+ out = {}
53
+ if self.version and self.version != "*":
54
+ out["version"] = self.version
55
+ if self.rev:
56
+ out["rev"] = self.rev
57
+ if self.git:
58
+ out["git"] = self.git
59
+ if self.modules:
60
+ out["modules"] = self.modules
61
+ return out or "*"
62
+
63
+ def __repr__(self):
64
+ ref = self.rev or self.version
65
+ return f"<Dependency {self.name}@{ref}>"
66
+
67
+
68
+ class Manifest:
69
+ def __init__(self, data, path=MANIFEST_NAME):
70
+ self.data = data or {}
71
+ self.path = path
72
+ self.name = self.data.get("name", "project")
73
+ self.dependencies = {
74
+ n: Dependency.from_spec(n, s)
75
+ for n, s in (self.data.get("dependencies") or {}).items()
76
+ }
77
+
78
+ @classmethod
79
+ def load(cls, path=MANIFEST_NAME):
80
+ if not os.path.exists(path):
81
+ raise ManifestError(
82
+ f"no {path} here. Run `lfpga init` to create one.")
83
+ with open(path) as f:
84
+ return cls(yaml.safe_load(f) or {}, path)
85
+
86
+ def add_dependency(self, dep: Dependency):
87
+ self.dependencies[dep.name] = dep
88
+ self.data.setdefault("dependencies", {})[dep.name] = dep.to_spec()
89
+
90
+ def remove_dependency(self, name):
91
+ self.dependencies.pop(name, None)
92
+ (self.data.get("dependencies") or {}).pop(name, None)
93
+
94
+ def save(self):
95
+ with open(self.path, "w") as f:
96
+ yaml.safe_dump(self.data, f, sort_keys=False,
97
+ default_flow_style=False)
98
+
99
+ @staticmethod
100
+ def scaffold(name):
101
+ return {
102
+ "name": name,
103
+ "top_module": "",
104
+ "filesets": {"rtl": {"files": [], "include_dirs": []}},
105
+ "dependencies": {},
106
+ }
lfpga/registry.py ADDED
@@ -0,0 +1,43 @@
1
+ """Talk to the LibFPGA registry (https://libfpga.com/cores/).
2
+
3
+ The registry resolves a package name to its source repo, license and
4
+ earned verification badges. This is a thin HTTP client; the registry does
5
+ the curation, ownership and verification.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import urllib.error
12
+ import urllib.request
13
+
14
+ DEFAULT_REGISTRY = os.environ.get("LFPGA_REGISTRY", "https://libfpga.com")
15
+
16
+
17
+ class RegistryError(Exception):
18
+ pass
19
+
20
+
21
+ class RegistryClient:
22
+ def __init__(self, base=None):
23
+ self.base = (base or DEFAULT_REGISTRY).rstrip("/")
24
+
25
+ def package(self, slug):
26
+ """Return the package metadata dict, or raise RegistryError."""
27
+ url = f"{self.base}/cores/{slug}.json"
28
+ req = urllib.request.Request(url, headers={"User-Agent": "lfpga"})
29
+ try:
30
+ with urllib.request.urlopen(req, timeout=20) as r:
31
+ data = json.loads(r.read().decode())
32
+ except urllib.error.HTTPError as e:
33
+ if e.code == 404:
34
+ raise RegistryError(
35
+ f"package '{slug}' is not in the registry "
36
+ f"({self.base}/cores/). Check the name, or add it with "
37
+ f"a git: URL.")
38
+ raise RegistryError(f"registry HTTP {e.code} for '{slug}'")
39
+ except urllib.error.URLError as e:
40
+ raise RegistryError(f"cannot reach the registry: {e.reason}")
41
+ if data.get("error"):
42
+ raise RegistryError(f"package '{slug}' not found")
43
+ return data
lfpga/resolve.py ADDED
@@ -0,0 +1,104 @@
1
+ """Resolve manifest dependencies into a concrete, pinned set of packages.
2
+
3
+ Phase 0: each dependency is fetched and pinned to an exact commit; its
4
+ build sources are read from the repo's own libfpga.yaml (the same manifest
5
+ the registry verifies). SemVer range solving over registry tags is Phase 1;
6
+ today, ranges resolve to the default branch and the lockfile pins the sha.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import glob
11
+ import os
12
+
13
+ import yaml
14
+
15
+ from .fetch import fetch
16
+ from .manifest import MANIFEST_NAME
17
+ from .registry import RegistryClient
18
+
19
+ # filesets that are NOT build inputs (testbenches, sim-only)
20
+ _SIM_FILESETS = {"sim", "tb", "test", "testbench", "formal"}
21
+
22
+
23
+ class LockedPackage:
24
+ def __init__(self, name, source, rev, files, include_dirs,
25
+ verified=None, note=""):
26
+ self.name = name
27
+ self.source = source
28
+ self.rev = rev
29
+ self.files = files
30
+ self.include_dirs = include_dirs
31
+ self.verified = verified or {}
32
+ self.note = note # e.g. "globbed (no libfpga.yaml)"
33
+ self.path = None # cache path, set at fetch time
34
+
35
+
36
+ def _read_repo_sources(path, modules):
37
+ """Return (files, include_dirs, note) for the build (synth) sources of a
38
+ fetched repo, read from its libfpga.yaml or globbed as a fallback."""
39
+ mpath = os.path.join(path, MANIFEST_NAME)
40
+ files, incs, note = [], [], ""
41
+ if os.path.exists(mpath):
42
+ with open(mpath) as f:
43
+ m = yaml.safe_load(f) or {}
44
+ filesets = m.get("filesets")
45
+ if isinstance(filesets, dict):
46
+ for key, fs in filesets.items():
47
+ if key.lower() in _SIM_FILESETS or not isinstance(fs, dict):
48
+ continue
49
+ files += fs.get("files", [])
50
+ incs += fs.get("include_dirs", [])
51
+ if not files: # flat registry-style manifest
52
+ files += m.get("files", [])
53
+ if m.get("top") and m["top"] not in files:
54
+ files.append(m["top"])
55
+ incs += m.get("include_dirs", [])
56
+ if not files:
57
+ files = _glob_sources(path)
58
+ note = "no libfpga.yaml: globbed HDL sources"
59
+ if modules:
60
+ picked = [f for f in files
61
+ if os.path.splitext(os.path.basename(f))[0] in modules]
62
+ if picked:
63
+ files = picked
64
+ else:
65
+ note = (note + "; " if note else "") + \
66
+ f"modules {modules} not matched, kept all"
67
+ # de-dup, preserve order
68
+ files = list(dict.fromkeys(files))
69
+ incs = list(dict.fromkeys(incs))
70
+ return files, incs, note
71
+
72
+
73
+ def _glob_sources(path):
74
+ for pat in ("rtl/**/*.sv", "rtl/**/*.v", "src/**/*.sv", "src/**/*.v",
75
+ "*.sv", "*.v"):
76
+ hits = sorted(glob.glob(os.path.join(path, pat), recursive=True))
77
+ hits = [os.path.relpath(h, path) for h in hits
78
+ if "/tb" not in h and "/test" not in h]
79
+ if hits:
80
+ return hits
81
+ return []
82
+
83
+
84
+ def resolve(manifest, registry=None):
85
+ """Resolve every dependency to a LockedPackage. Returns the list."""
86
+ registry = registry or RegistryClient()
87
+ locked = []
88
+ for name, dep in manifest.dependencies.items():
89
+ verified = {}
90
+ if dep.git:
91
+ url = dep.git
92
+ else:
93
+ info = registry.package(name)
94
+ url = info.get("source_url")
95
+ if not url:
96
+ raise RuntimeError(
97
+ f"'{name}' has no source_url in the registry")
98
+ verified = info.get("verification", {})
99
+ path, commit = fetch(name, url, dep.rev)
100
+ files, incs, note = _read_repo_sources(path, dep.modules)
101
+ pkg = LockedPackage(name, url, commit, files, incs, verified, note)
102
+ pkg.path = path
103
+ locked.append(pkg)
104
+ return locked
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: lfpga
3
+ Version: 0.1.0
4
+ Summary: A package manager for FPGA/HDL development: resolve, fetch and assemble IP cores from the LibFPGA registry.
5
+ Author: Antonio Roldao
6
+ License: MIT
7
+ Project-URL: Homepage, https://libfpga.com/cores/
8
+ Project-URL: Registry, https://libfpga.com/cores/
9
+ Project-URL: Source, https://github.com/libfpga/lfpga
10
+ Keywords: fpga,hdl,verilog,vhdl,package-manager,ip-cores
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Build Tools
18
+ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: PyYAML>=6.0
23
+ Dynamic: license-file
24
+
25
+ # lfpga
26
+
27
+ A package manager for FPGA/HDL development. Resolve, fetch and assemble
28
+ open IP cores from the [LibFPGA registry](https://libfpga.com/cores/) into a
29
+ source list your simulator or synthesis tool can consume.
30
+
31
+ The registry is the `pypi.org` of FPGA IP: curated, ownership-claimed, and
32
+ toolchain-verified. **`lfpga` is the `pip`**, and its superpower is that
33
+ every package can carry an *earned* verification badge (lints clean,
34
+ synthesizes, testbench passes).
35
+
36
+ ```console
37
+ $ lfpga init my-soc
38
+ $ lfpga add libfpga
39
+ Found libfpga: https://github.com/libfpga/libfpga
40
+ license MIT · verilog · ✓ lint, synth, testbench, formal
41
+ $ lfpga add picorv32
42
+ $ lfpga install
43
+ libfpga a4ef4a3fa4ac 1 files ✓ lint, synth, testbench, formal
44
+ picorv32 e9c4c5b8... 1 files unverified
45
+
46
+ Wrote lfpga.lock and build/sources.f (2 source files).
47
+ ```
48
+
49
+ Then point your tool at the generated filelist:
50
+
51
+ ```console
52
+ $ verilator --lint-only -f build/sources.f
53
+ $ iverilog -o sim.vvp -f build/sources.f
54
+ # Vivado: read_verilog -f build/sources.f (via -f)
55
+ ```
56
+
57
+ ## Why a package manager for FPGA is different
58
+
59
+ FPGA IP is *source*, not binaries: there is no ABI and no linking, so
60
+ `lfpga` vendors declared HDL files and hands them to the tool of your
61
+ choice. It does not replace your simulator or synthesizer; it produces the
62
+ inputs they expect. See the
63
+ [design notes](https://github.com/libfpga/lfpga/blob/main/docs/DESIGN.md)
64
+ for the full rationale (name collisions, fuzzy versioning, filesets,
65
+ vendor primitives).
66
+
67
+ ## Install
68
+
69
+ ```console
70
+ pip install lfpga # Python 3.11+
71
+ ```
72
+
73
+ ## Commands
74
+
75
+ | Command | What it does |
76
+ | --- | --- |
77
+ | `lfpga init [name]` | Create a `libfpga.yaml` here |
78
+ | `lfpga add <pkg>` | Add a dependency (`name`, `name@rev`, or a git URL) |
79
+ | `lfpga install` | Resolve, pin (`lfpga.lock`), fetch, and write `build/sources.f` |
80
+ | `lfpga list` | Show the locked dependencies and their badges |
81
+ | `lfpga sources [--format verilator]` | Emit the assembled source list |
82
+
83
+ ## The manifest (`libfpga.yaml`)
84
+
85
+ One file, like `Cargo.toml`: it both declares your dependencies and (if you
86
+ publish) describes this repo as a package for the registry.
87
+
88
+ ```yaml
89
+ name: my-soc
90
+ dependencies:
91
+ libfpga: "*" # latest default branch
92
+ picorv32: { rev: v1.0.3 } # pin a tag, branch or commit
93
+ libfpga-myhdl: { modules: [lfpga_mac] } # sub-select from a repo
94
+ private-mac: { git: "https://github.com/acme/mac.git" }
95
+ ```
96
+
97
+ ## The lockfile (`lfpga.lock`)
98
+
99
+ Commit it. Because HDL builds are source-and-elaborate, reproducibility is
100
+ lockfile-first: it pins the exact commit and the exact build sources, so a
101
+ build is identical across machines and over time.
102
+
103
+ ## Status
104
+
105
+ Phase 0 (MVP): manifest, lockfile, git fetch, and a filelist emitter, all
106
+ against the live registry. On the roadmap: SemVer range resolution over
107
+ registry tags, sim/synth/formal filesets, Edalize/FuseSoC integration
108
+ (`lfpga sim` / `lfpga synth`), and `lfpga publish` gated by the verification
109
+ toolchain.
110
+
111
+ MIT licensed. Part of [LibFPGA](https://libfpga.com).
@@ -0,0 +1,14 @@
1
+ lfpga/__init__.py,sha256=P0A_Wp6b-5mazCyjWFP7pMUc5EkD9CP-lVv17_jeoy4,292
2
+ lfpga/cli.py,sha256=Ybt5AxGxr5gWFRw6CPmOaZbA0_qHBJi4QEFZFpy8Frg,6288
3
+ lfpga/emit.py,sha256=8pKXxmxPF76l0wAlRDrCOpUj8ZJfcT0u2kyKyeou6Zk,1429
4
+ lfpga/fetch.py,sha256=QbZOzBOZOP2yM0CP8wL70spWAgqhJ5mU97rSJUpB_Dk,2316
5
+ lfpga/lockfile.py,sha256=-iXTAN0cBCToYjSDaKAiPCUzkD58NWvddwb1cUOjhw0,1952
6
+ lfpga/manifest.py,sha256=nDsva3wpS0RiWuyGwy6OCivYp80SpROwzjbRyhvYlkk,3370
7
+ lfpga/registry.py,sha256=hO9SKY8eaGGj-Mhj0fB5PMS2Of8pLKPxtBEe8N1Ny-A,1532
8
+ lfpga/resolve.py,sha256=o_Ozb9vQe7qlhJV2t84VHkZl0U8Y1JjNy2du5ReoSGk,3809
9
+ lfpga-0.1.0.dist-info/licenses/LICENSE,sha256=D3rKgKS0RY4ykVanhRoitdhaYof-df7BAqsSVIacb-A,1078
10
+ lfpga-0.1.0.dist-info/METADATA,sha256=Gke8a-mUaeuGoKVvcnZrWnWXJItQ-IvtEoBm2CXO7vY,4035
11
+ lfpga-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
12
+ lfpga-0.1.0.dist-info/entry_points.txt,sha256=qjAZ7HLgomgGF6-hjirEDrErQGYEZ9BEz8RRYIJnieY,41
13
+ lfpga-0.1.0.dist-info/top_level.txt,sha256=IeX0gkzxQJvVhpnugWfBOGgbXu29SCtO_ZGejcmvZ_Q,6
14
+ lfpga-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lfpga = lfpga.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antonio Roldao, Ph.D.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ lfpga