Skip to content

Commit 77b33c9

Browse files
author
Tim Hutt
committed
Reorganise Python code using pyproject.toml
Add pyproject.toml (the modern alternative to requirements.txt), making this a proper Python package that can be installed via pip and potentially uploaded to PyPI. This also loads the files using `importlib.resources` and installs them into the wheel. This means that when you create a wheel using `uv build` it will still be able to load all the opcodes and CSV files. To avoid moving those resource files in the source repo, the Python build backend (hatchling) is instructed to move them to the right place when building a wheel, and the `resource_root()` function checks in both places so it always works. This is a little hacky but it works. CI builds source and binary wheels (not actually binary) that can be uploaded to PyPI. If we do upload them then using this project is as simple as ``` uvx riscv_opcodes -c 'rv*' ```
1 parent c35a8ed commit 77b33c9

23 files changed

Lines changed: 1443 additions & 155 deletions

.github/workflows/python-app.yml

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
python-version: ['3.9', '3.10', '3.11', '3.12','3.13']
14+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
1515

1616
steps:
1717
- uses: actions/checkout@v4
@@ -29,20 +29,37 @@ jobs:
2929
restore-keys: |
3030
${{ runner.os }}-pre-commit-
3131
32-
- name: Install dependencies
33-
run: python3 -m pip install pre-commit coverage matplotlib
32+
- name: Install uv
33+
uses: astral-sh/setup-uv@v6
34+
with:
35+
version: "0.8.22"
36+
37+
- name: Install pre-commit
38+
run: uv tool install pre-commit
3439

3540
- name: Run pre-commit
3641
run: pre-commit run --all-files
3742

43+
# Generate all output with code coverage. Unfortunately due to limitations in
44+
# the `coverage` tool we need to run `riscv_opcodes` as a module.
3845
- name: Generate
39-
run: coverage run ./parse.py -c -chisel -sverilog -rust -latex -spinalhdl -svg -go "rv*" "unratified/rv*"
46+
run: uv run -- coverage run -m riscv_opcodes -c -chisel -sverilog -rust -latex -spinalhdl -svg -go "rv*" "unratified/rv*"
4047

4148
- name: Check C output
4249
run: cat encoding.out.h | cpp
4350

4451
- name: Generate coverage
45-
run: coverage xml
52+
run: uv run -- coverage xml
4653

4754
- name: Upload coverage
4855
uses: codecov/codecov-action@v4
56+
57+
- name: Build wheels
58+
run: uv build
59+
60+
- name: Upload wheels
61+
if: matrix.python-version == '3.13'
62+
uses: actions/upload-artifact@v4
63+
with:
64+
name: dist
65+
path: dist

.github/workflows/tests.yml

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,16 @@ jobs:
2121
with:
2222
python-version: ${{ matrix.python-version }}
2323

24-
- name: Cache pip dependencies
25-
uses: actions/cache@v3
24+
- name: Install uv
25+
uses: astral-sh/setup-uv@v6
2626
with:
27-
path: ~/.cache/pip
28-
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-coverage
29-
restore-keys: |
30-
${{ runner.os }}-pip-${{ matrix.python-version }}-
31-
32-
- name: Install dependencies
33-
run: |
34-
pip3 install coverage
27+
version: "0.8.22"
3528

3629
- name: Test error outputs
37-
run: coverage run -m unittest -b
30+
run: uv run -- coverage run -m unittest -b tests/test.py
3831

3932
- name: Generate coverage report
40-
run: coverage xml
33+
run: uv run -- coverage xml
4134

4235
- name: Upload coverage report
4336
uses: codecov/codecov-action@v4

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ inst.svg
1212
instr_dict.json
1313

1414
__pycache__/
15+
16+
# Default uv virtual environment location.
17+
/.venv
18+
19+
# Generated by `coverage` tool.
20+
/.coverage
21+
/coverage.xml

.pylintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ disable=
2525
missing-module-docstring,
2626
missing-function-docstring,
2727
missing-class-docstring,
28+
# Unfortunately pylint doesn't respect py-version when warning
29+
# about deprecated items. See https://github.com/pylint-dev/pylint/issues/10608
30+
deprecated-class,
2831

2932
# These names are fine when used sensibly. Without listing them here
3033
# Pylint will complain they are too short.

Makefile

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,34 @@ INSTALL_HEADER_FILES := $(ISASIM_H) $(PK_H) $(ENV_H) $(OPENOCD_H)
77
PSEUDO_FLAG := $(if $(PSEUDO),-pseudo,)
88

99

10+
.PHONY : default
1011
default: everything
1112

12-
.PHONY: everything encoding.out.h inst.chisel inst.go latex inst.sverilog inst.rs clean install instr-table.tex priv-instr-table.tex inst.spinalhdl pseudo
13+
.PHONY: everything encoding.out.h inst.chisel inst.go latex inst.sverilog inst.rs clean install instr-table.tex priv-instr-table.tex inst.spinalhdl pseudo test
1314

1415
pseudo:
1516
@$(MAKE) PSEUDO=1 everything
1617

1718
everything:
18-
@./parse.py $(PSEUDO_FLAG) -c -go -chisel -sverilog -rust -latex -spinalhdl $(EXTENSIONS)
19+
@uv run riscv_opcodes $(PSEUDO_FLAG) -c -go -chisel -sverilog -rust -latex -spinalhdl $(EXTENSIONS)
1920

2021
encoding.out.h:
21-
@./parse.py -c $(PSEUDO_FLAG) $(EXTENSIONS)
22+
@uv run riscv_opcodes -c $(PSEUDO_FLAG) $(EXTENSIONS)
2223

2324
inst.chisel:
24-
@./parse.py -chisel $(PSEUDO_FLAG) $(EXTENSIONS)
25+
@uv run riscv_opcodes -chisel $(PSEUDO_FLAG) $(EXTENSIONS)
2526

2627
inst.go:
27-
@./parse.py -go $(PSEUDO_FLAG) $(EXTENSIONS)
28+
@uv run riscv_opcodes -go $(PSEUDO_FLAG) $(EXTENSIONS)
2829

2930
latex:
30-
@./parse.py -latex $(PSEUDO_FLAG) $(EXTENSIONS)
31+
@uv run riscv_opcodes -latex $(PSEUDO_FLAG) $(EXTENSIONS)
3132

3233
inst.sverilog:
33-
@./parse.py -sverilog $(PSEUDO_FLAG) $(EXTENSIONS)
34+
@uv run riscv_opcodes -sverilog $(PSEUDO_FLAG) $(EXTENSIONS)
3435

3536
inst.rs:
36-
@./parse.py -rust $(PSEUDO_FLAG) $(EXTENSIONS)
37+
@uv run riscv_opcodes -rust $(PSEUDO_FLAG) $(EXTENSIONS)
3738

3839
clean:
3940
rm -f inst* priv-instr-table.tex encoding.out.h
@@ -44,9 +45,12 @@ install: everything
4445
cp -f encoding.out.h $$FILE; \
4546
done
4647

48+
test:
49+
@uv run -m unittest -b tests/test.py
50+
4751
instr-table.tex: latex
4852

4953
priv-instr-table.tex: latex
5054

5155
inst.spinalhdl:
52-
@./parse.py -spinalhdl $(PSEUDO_FLAG) $(EXTENSIONS)
56+
@uv run riscv_opcodes -spinalhdl $(PSEUDO_FLAG) $(EXTENSIONS)

README.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,19 @@ tools and projects like Spike, PK, RISC-V Manual, etc.
1010
## Project Structure
1111

1212
```bash
13-
├── constants.py # contains variables, constants and data-structures used in parse.py
14-
├── encoding.h # the template encoding.h file
15-
├── LICENSE # license file
16-
├── Makefile # makefile to generate artifacts
17-
├── parse.py # python file to perform checks on the instructions and generate artifacts
18-
├── README.md # this file
19-
├── rv* # instruction opcode files
20-
└── unratified # contains unratified instruction opcode files
13+
├── extensions # instruction opcodes
14+
├── extensions/unratified # unratified instruction opcodes
15+
├── encoding.h # the template encoding.h file
16+
├── Makefile # makefile to generate artifacts
17+
└── src/riscv_opcodes # python files to perform checks on
18+
# the instructions and generate artifacts
2119
```
2220

2321
## File Naming Policy
2422

2523
This project follows a very specific file structure to define the instruction encodings. All files
2624
containing instruction encodings start with the prefix `rv`. These files can either be present in
27-
the root directory (if the instructions have been ratified) or the `unratified` directory. The exact
25+
the `extensions` directory (if the instructions have been ratified) or the `extensions/unratified` directory. The exact
2826
file-naming policy and location is as mentioned below:
2927

3028
1. `rv_x` - contains instructions common within the 32-bit and 64-bit modes of extension X.
@@ -140,10 +138,11 @@ The following artifacts can be generated using parse.py:
140138
- inst.spinalhdl : spinalhdl code to decode instructions
141139
- inst.go : go code to decode instructions
142140

143-
To generate all the above artifacts for all instructions currently checked in, simply run `make` from the root-directory. This should print the following log on the command-line:
141+
To generate all the above artifacts for all instructions currently checked in, simply run `make` from the root-directory. [`uv`](https://docs.astral.sh/uv/) is required (see [easy installation instructions](https://docs.astral.sh/uv/getting-started/installation/)).
142+
143+
`make` should print the following log on the command-line:
144144

145145
```
146-
Running with args : ['./parse.py', '-c', '-go', '-chisel', '-sverilog', '-rust', '-latex', '-spinalhdl', 'rv*', 'unratified/rv*']
147146
Extensions selected : ['rv*', 'unratified/rv*']
148147
INFO:: encoding.out.h generated successfully
149148
INFO:: inst.chisel generated successfully
@@ -165,7 +164,6 @@ make EXTENSIONS='rv*_i rv*_m'
165164
Which will print the following log:
166165

167166
```
168-
Running with args : ['./parse.py', '-c', '-go', '-chisel', '-sverilog', '-rust', '-latex', '-spinalhdl', 'rv32_i', 'rv64_i', 'rv_i', 'rv64_m', 'rv_m']
169167
Extensions selected : ['rv32_i', 'rv64_i', 'rv_i', 'rv64_m', 'rv_m']
170168
INFO:: encoding.out.h generated successfully
171169
INFO:: inst.chisel generated successfully
@@ -181,7 +179,7 @@ If you only want a specific artifact you can use one or more of the following ta
181179
For example, if you want to generate the `c` based artifact with extensions as shown earlier, you can use the following command:
182180

183181
```bash
184-
./parse.py -c rv*_i rv*_m
182+
uv run riscv_opcodes -c 'rv*_i' 'rv*_m'
185183
```
186184
Which will print the following log:
187185

@@ -202,7 +200,6 @@ You can use the `clean` target to remove all artifacts.
202200
To add a new extension of instructions, create an appropriate `rv*` file based on the policy defined in [File Structure](#file-naming-policy). Run `make` from the root directory to ensure that all checks pass and all artifacts are created correctly. A successful run should print the following log on the terminal:
203201

204202
```
205-
Running with args : ['./parse.py', '-c', '-chisel', '-sverilog', '-rust', '-latex', 'rv*', 'unratified/rv*']
206203
Extensions selected : ['rv*', 'unratified/rv*']
207204
INFO:: encoding.out.h generated successfully
208205
INFO:: inst.chisel generated successfully
@@ -216,7 +213,7 @@ Create a PR for review.
216213

217214
## Enabling Debug logs in parse.py
218215

219-
To enable debug logs in parse.py change `level=logging.INFO` to `level=logging.DEBUG` and run the python command. You will now see debug statements on
216+
To enable debug logs in `parse.py` change `level=logging.INFO` to `level=logging.DEBUG` and run the python command. You will now see debug statements on
220217
the terminal like below:
221218
```
222219
DEBUG:: Collecting standard instructions first

pyproject.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[project]
2+
name = "riscv-opcodes"
3+
version = "0.1.0"
4+
description = "Machine readable information about RISC-V instruction opcodes"
5+
readme = "README.md"
6+
requires-python = ">= 3.9"
7+
dependencies = [
8+
"matplotlib>=3.9.0, <4",
9+
]
10+
11+
[dependency-groups]
12+
dev = [
13+
# Code coverage tool, used in CI.
14+
"coverage >=7,<8"
15+
]
16+
17+
[project.scripts]
18+
riscv_opcodes = "riscv_opcodes.parse:main"
19+
20+
[build-system]
21+
requires = ["hatchling"]
22+
build-backend = "hatchling.build"
23+
24+
[tool.hatch.build.targets.wheel.force-include]
25+
"extensions" = "riscv_opcodes/extensions"
26+
"arg_lut.csv" = "riscv_opcodes/arg_lut.csv"
27+
"causes.csv" = "riscv_opcodes/causes.csv"
28+
"csrs.csv" = "riscv_opcodes/csrs.csv"
29+
"csrs32.csv" = "riscv_opcodes/csrs32.csv"
30+
"encoding.h" = "riscv_opcodes/encoding.h"

src/riscv_opcodes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Mark this directory as a package. This is not actually needed by
2+
# Python but Pylint gets confused about relative imports without it.

src/riscv_opcodes/__main__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
This allows running as a module, i.e. `python3 -m riscv_opcodes` which
3+
we wouldn't normally need, but the `coverage` tool doesn't work on
4+
installed scripts - you can't do `coverage run riscv_opcodes` because it
5+
looks for a Python file called `riscv_opcodes` in the current directory.
6+
"""
7+
8+
from .parse import main
9+
10+
main()

c_utils.py renamed to src/riscv_opcodes/c_utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import os
33
import pprint
44

5-
from constants import causes, csrs, csrs32
6-
from shared_utils import InstrDict, arg_lut
5+
from .constants import causes, csrs, csrs32
6+
from .resources import read_text_resource
7+
from .shared_utils import InstrDict, arg_lut
78

89
pp = pprint.PrettyPrinter(indent=2)
910
logging.basicConfig(level=logging.INFO, format="%(levelname)s:: %(message)s")
@@ -43,8 +44,7 @@ def make_c(instr_dict: InstrDict):
4344
mask = ((1 << (end - begin + 1)) - 1) << begin
4445
arg_str += f"#define INSN_FIELD_{sanitized_name.upper()} {hex(mask)}\n"
4546

46-
with open(f"{os.path.dirname(__file__)}/encoding.h", "r", encoding="utf-8") as file:
47-
enc_header = file.read()
47+
enc_header = read_text_resource("encoding.h")
4848

4949
commit = os.popen('git log -1 --format="format:%h"').read()
5050

0 commit comments

Comments
 (0)