lfpga 0.1.0__tar.gz
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-0.1.0/LICENSE +21 -0
- lfpga-0.1.0/PKG-INFO +111 -0
- lfpga-0.1.0/README.md +87 -0
- lfpga-0.1.0/lfpga/__init__.py +8 -0
- lfpga-0.1.0/lfpga/cli.py +194 -0
- lfpga-0.1.0/lfpga/emit.py +46 -0
- lfpga-0.1.0/lfpga/fetch.py +77 -0
- lfpga-0.1.0/lfpga/lockfile.py +57 -0
- lfpga-0.1.0/lfpga/manifest.py +106 -0
- lfpga-0.1.0/lfpga/registry.py +43 -0
- lfpga-0.1.0/lfpga/resolve.py +104 -0
- lfpga-0.1.0/lfpga.egg-info/PKG-INFO +111 -0
- lfpga-0.1.0/lfpga.egg-info/SOURCES.txt +18 -0
- lfpga-0.1.0/lfpga.egg-info/dependency_links.txt +1 -0
- lfpga-0.1.0/lfpga.egg-info/entry_points.txt +2 -0
- lfpga-0.1.0/lfpga.egg-info/requires.txt +1 -0
- lfpga-0.1.0/lfpga.egg-info/top_level.txt +1 -0
- lfpga-0.1.0/pyproject.toml +35 -0
- lfpga-0.1.0/setup.cfg +4 -0
- lfpga-0.1.0/tests/test_lfpga.py +105 -0
lfpga-0.1.0/LICENSE
ADDED
|
@@ -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.
|
lfpga-0.1.0/PKG-INFO
ADDED
|
@@ -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).
|
lfpga-0.1.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# lfpga
|
|
2
|
+
|
|
3
|
+
A package manager for FPGA/HDL development. Resolve, fetch and assemble
|
|
4
|
+
open IP cores from the [LibFPGA registry](https://libfpga.com/cores/) into a
|
|
5
|
+
source list your simulator or synthesis tool can consume.
|
|
6
|
+
|
|
7
|
+
The registry is the `pypi.org` of FPGA IP: curated, ownership-claimed, and
|
|
8
|
+
toolchain-verified. **`lfpga` is the `pip`**, and its superpower is that
|
|
9
|
+
every package can carry an *earned* verification badge (lints clean,
|
|
10
|
+
synthesizes, testbench passes).
|
|
11
|
+
|
|
12
|
+
```console
|
|
13
|
+
$ lfpga init my-soc
|
|
14
|
+
$ lfpga add libfpga
|
|
15
|
+
Found libfpga: https://github.com/libfpga/libfpga
|
|
16
|
+
license MIT · verilog · ✓ lint, synth, testbench, formal
|
|
17
|
+
$ lfpga add picorv32
|
|
18
|
+
$ lfpga install
|
|
19
|
+
libfpga a4ef4a3fa4ac 1 files ✓ lint, synth, testbench, formal
|
|
20
|
+
picorv32 e9c4c5b8... 1 files unverified
|
|
21
|
+
|
|
22
|
+
Wrote lfpga.lock and build/sources.f (2 source files).
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then point your tool at the generated filelist:
|
|
26
|
+
|
|
27
|
+
```console
|
|
28
|
+
$ verilator --lint-only -f build/sources.f
|
|
29
|
+
$ iverilog -o sim.vvp -f build/sources.f
|
|
30
|
+
# Vivado: read_verilog -f build/sources.f (via -f)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Why a package manager for FPGA is different
|
|
34
|
+
|
|
35
|
+
FPGA IP is *source*, not binaries: there is no ABI and no linking, so
|
|
36
|
+
`lfpga` vendors declared HDL files and hands them to the tool of your
|
|
37
|
+
choice. It does not replace your simulator or synthesizer; it produces the
|
|
38
|
+
inputs they expect. See the
|
|
39
|
+
[design notes](https://github.com/libfpga/lfpga/blob/main/docs/DESIGN.md)
|
|
40
|
+
for the full rationale (name collisions, fuzzy versioning, filesets,
|
|
41
|
+
vendor primitives).
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
pip install lfpga # Python 3.11+
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Commands
|
|
50
|
+
|
|
51
|
+
| Command | What it does |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `lfpga init [name]` | Create a `libfpga.yaml` here |
|
|
54
|
+
| `lfpga add <pkg>` | Add a dependency (`name`, `name@rev`, or a git URL) |
|
|
55
|
+
| `lfpga install` | Resolve, pin (`lfpga.lock`), fetch, and write `build/sources.f` |
|
|
56
|
+
| `lfpga list` | Show the locked dependencies and their badges |
|
|
57
|
+
| `lfpga sources [--format verilator]` | Emit the assembled source list |
|
|
58
|
+
|
|
59
|
+
## The manifest (`libfpga.yaml`)
|
|
60
|
+
|
|
61
|
+
One file, like `Cargo.toml`: it both declares your dependencies and (if you
|
|
62
|
+
publish) describes this repo as a package for the registry.
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
name: my-soc
|
|
66
|
+
dependencies:
|
|
67
|
+
libfpga: "*" # latest default branch
|
|
68
|
+
picorv32: { rev: v1.0.3 } # pin a tag, branch or commit
|
|
69
|
+
libfpga-myhdl: { modules: [lfpga_mac] } # sub-select from a repo
|
|
70
|
+
private-mac: { git: "https://github.com/acme/mac.git" }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## The lockfile (`lfpga.lock`)
|
|
74
|
+
|
|
75
|
+
Commit it. Because HDL builds are source-and-elaborate, reproducibility is
|
|
76
|
+
lockfile-first: it pins the exact commit and the exact build sources, so a
|
|
77
|
+
build is identical across machines and over time.
|
|
78
|
+
|
|
79
|
+
## Status
|
|
80
|
+
|
|
81
|
+
Phase 0 (MVP): manifest, lockfile, git fetch, and a filelist emitter, all
|
|
82
|
+
against the live registry. On the roadmap: SemVer range resolution over
|
|
83
|
+
registry tags, sim/synth/formal filesets, Edalize/FuseSoC integration
|
|
84
|
+
(`lfpga sim` / `lfpga synth`), and `lfpga publish` gated by the verification
|
|
85
|
+
toolchain.
|
|
86
|
+
|
|
87
|
+
MIT licensed. Part of [LibFPGA](https://libfpga.com).
|
|
@@ -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-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())
|
|
@@ -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
|
|
@@ -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}")
|
|
@@ -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
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -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,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
lfpga/__init__.py
|
|
5
|
+
lfpga/cli.py
|
|
6
|
+
lfpga/emit.py
|
|
7
|
+
lfpga/fetch.py
|
|
8
|
+
lfpga/lockfile.py
|
|
9
|
+
lfpga/manifest.py
|
|
10
|
+
lfpga/registry.py
|
|
11
|
+
lfpga/resolve.py
|
|
12
|
+
lfpga.egg-info/PKG-INFO
|
|
13
|
+
lfpga.egg-info/SOURCES.txt
|
|
14
|
+
lfpga.egg-info/dependency_links.txt
|
|
15
|
+
lfpga.egg-info/entry_points.txt
|
|
16
|
+
lfpga.egg-info/requires.txt
|
|
17
|
+
lfpga.egg-info/top_level.txt
|
|
18
|
+
tests/test_lfpga.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyYAML>=6.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lfpga
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lfpga"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A package manager for FPGA/HDL development: resolve, fetch and assemble IP cores from the LibFPGA registry."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Antonio Roldao" }]
|
|
13
|
+
keywords = ["fpga", "hdl", "verilog", "vhdl", "package-manager", "ip-cores"]
|
|
14
|
+
dependencies = ["PyYAML>=6.0"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Build Tools",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://libfpga.com/cores/"
|
|
28
|
+
Registry = "https://libfpga.com/cores/"
|
|
29
|
+
Source = "https://github.com/libfpga/lfpga"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
lfpga = "lfpga.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
packages = ["lfpga"]
|
lfpga-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Offline tests for lfpga (no network / git needed)."""
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from lfpga import emit, lockfile
|
|
9
|
+
from lfpga.manifest import Dependency, Manifest
|
|
10
|
+
from lfpga.resolve import LockedPackage, _read_repo_sources
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestManifest(unittest.TestCase):
|
|
14
|
+
def test_scaffold_and_add_roundtrip(self):
|
|
15
|
+
m = Manifest(Manifest.scaffold("demo"))
|
|
16
|
+
m.add_dependency(Dependency("picorv32", rev="v1.0.3"))
|
|
17
|
+
m.add_dependency(Dependency("libfpga"))
|
|
18
|
+
with tempfile.TemporaryDirectory() as d:
|
|
19
|
+
m.path = os.path.join(d, "libfpga.yaml")
|
|
20
|
+
m.save()
|
|
21
|
+
back = Manifest.load(m.path)
|
|
22
|
+
self.assertEqual(back.name, "demo")
|
|
23
|
+
self.assertEqual(back.dependencies["picorv32"].rev, "v1.0.3")
|
|
24
|
+
self.assertEqual(back.dependencies["libfpga"].version, "*")
|
|
25
|
+
|
|
26
|
+
def test_dep_spec_forms(self):
|
|
27
|
+
self.assertEqual(Dependency.from_spec("a", None).version, "*")
|
|
28
|
+
self.assertEqual(Dependency.from_spec("a", "^1.0").version, "^1.0")
|
|
29
|
+
d = Dependency.from_spec("a", {"rev": "abc", "modules": ["m"]})
|
|
30
|
+
self.assertEqual(d.rev, "abc")
|
|
31
|
+
self.assertEqual(d.modules, ["m"])
|
|
32
|
+
|
|
33
|
+
def test_git_dep_roundtrips(self):
|
|
34
|
+
d = Dependency("x", git="https://example.com/x.git")
|
|
35
|
+
self.assertEqual(d.to_spec(), {"git": "https://example.com/x.git"})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestLockfile(unittest.TestCase):
|
|
39
|
+
def test_dump_load_roundtrip(self):
|
|
40
|
+
pkgs = [LockedPackage("libfpga", "https://h/libfpga",
|
|
41
|
+
"a" * 40, ["rtl/crc.v"], ["rtl"],
|
|
42
|
+
{"lint": True, "synth": True}, "")]
|
|
43
|
+
with tempfile.TemporaryDirectory() as d:
|
|
44
|
+
p = os.path.join(d, "lfpga.lock")
|
|
45
|
+
lockfile.dump(pkgs, p)
|
|
46
|
+
back = lockfile.load(p)
|
|
47
|
+
self.assertEqual(len(back), 1)
|
|
48
|
+
self.assertEqual(back[0].name, "libfpga")
|
|
49
|
+
self.assertEqual(back[0].rev, "a" * 40)
|
|
50
|
+
self.assertEqual(back[0].files, ["rtl/crc.v"])
|
|
51
|
+
self.assertTrue(back[0].verified.get("lint"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestEmit(unittest.TestCase):
|
|
55
|
+
def test_filelist(self):
|
|
56
|
+
pkg = LockedPackage("libfpga", "u", "abc123def456",
|
|
57
|
+
["rtl/crc.v"], ["rtl"])
|
|
58
|
+
pkg.path = "/cache/libfpga"
|
|
59
|
+
text = emit.filelist([pkg])
|
|
60
|
+
self.assertIn("+incdir+/cache/libfpga/rtl", text)
|
|
61
|
+
self.assertIn("/cache/libfpga/rtl/crc.v", text)
|
|
62
|
+
self.assertIn("libfpga @ abc123def456", text)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestReadRepoSources(unittest.TestCase):
|
|
66
|
+
def _repo(self, d, manifest):
|
|
67
|
+
os.makedirs(os.path.join(d, "rtl"))
|
|
68
|
+
for name in ("a.v", "b.v"):
|
|
69
|
+
open(os.path.join(d, "rtl", name), "w").close()
|
|
70
|
+
if manifest is not None:
|
|
71
|
+
with open(os.path.join(d, "libfpga.yaml"), "w") as f:
|
|
72
|
+
yaml.safe_dump(manifest, f)
|
|
73
|
+
|
|
74
|
+
def test_flat_manifest(self):
|
|
75
|
+
with tempfile.TemporaryDirectory() as d:
|
|
76
|
+
self._repo(d, {"files": ["rtl/a.v"], "include_dirs": ["rtl"]})
|
|
77
|
+
files, incs, note = _read_repo_sources(d, [])
|
|
78
|
+
self.assertEqual(files, ["rtl/a.v"])
|
|
79
|
+
self.assertEqual(incs, ["rtl"])
|
|
80
|
+
self.assertEqual(note, "")
|
|
81
|
+
|
|
82
|
+
def test_filesets_excludes_sim(self):
|
|
83
|
+
with tempfile.TemporaryDirectory() as d:
|
|
84
|
+
self._repo(d, {"filesets": {
|
|
85
|
+
"rtl": {"files": ["rtl/a.v"], "include_dirs": ["rtl"]},
|
|
86
|
+
"sim": {"files": ["rtl/b.v"]}}})
|
|
87
|
+
files, incs, note = _read_repo_sources(d, [])
|
|
88
|
+
self.assertEqual(files, ["rtl/a.v"]) # sim fileset dropped
|
|
89
|
+
|
|
90
|
+
def test_glob_fallback_when_no_manifest(self):
|
|
91
|
+
with tempfile.TemporaryDirectory() as d:
|
|
92
|
+
self._repo(d, None)
|
|
93
|
+
files, incs, note = _read_repo_sources(d, [])
|
|
94
|
+
self.assertIn("rtl/a.v", files)
|
|
95
|
+
self.assertIn("globbed", note)
|
|
96
|
+
|
|
97
|
+
def test_module_subselect(self):
|
|
98
|
+
with tempfile.TemporaryDirectory() as d:
|
|
99
|
+
self._repo(d, {"files": ["rtl/a.v", "rtl/b.v"]})
|
|
100
|
+
files, _, _ = _read_repo_sources(d, ["a"])
|
|
101
|
+
self.assertEqual(files, ["rtl/a.v"])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
unittest.main()
|