Skip to content

Commit 286c0b6

Browse files
initial release tooling for buck2 builds (#297)
Some light docs and this kinda seems to work so fire in the hole.
1 parent ec6a558 commit 286c0b6

12 files changed

+485
-2
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ vivado*.log
3434
/clockinfo.txt
3535
**/*clockInfo.txt
3636
*.txt
37+
*.zip
38+
*.fst
39+
*.vcd
3740
# Yep, even ignore Vivado core dump logs!
3841
*.log
3942

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ and use the shell to execute each line:
109109

110110
`buck2 bxl //tools/vunit-sims.bxl:vunit_sim_gen | while IFS= read -r line; do eval "$line" ; done`
111111

112+
## Release tooling
113+
There's a rudimentary FPGA releaser tool that can be run as follows:
114+
`buck2 run //tools/fpga_releaser:cli -- --fpga <fpga-name> --hubris <path to root of hubris checkout>`
115+
This tool will fetch the latest main build of your chosen FPGA, download the build archive,
116+
collect the relevant files, create a GitHub releaes, copy the hubris-impacting files into the
117+
appropriate location in hubris (for you to manually commit), write out the hubris README.md
118+
for these files and print some timing and utilization statistics.
119+
`--skip-gh` will skip doing the github release (mostly for testing)
120+
`--token` you can pass your github token in, your env will be checked for `GITHUB_TOKEN` if this is
121+
empty.
122+
123+
Config information is stored in `tools/fpga_releaser/config.toml` which controls the fpga name
124+
to build image mapping, toolchain info, and hubris subdirectory information for each build.
125+
126+
Currently, only buck2-based build archives are able to be processed, cobble-based stuff is
127+
not implemented.
112128

113129
## multitool
114130
multitool is a collection of quality of live utilities built in-tree for regular use, but whose

tools/fpga_releaser/BUCK

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
python_binary(
2+
name = 'cli',
3+
main = 'cli.py',
4+
deps = [':fpga_releaser_lib'],
5+
visibility = ["PUBLIC"],
6+
)
7+
8+
python_library(
9+
name = 'fpga_releaser_lib',
10+
srcs = glob(["*.py"]),
11+
base_module = "fpga_releaser",
12+
resources = glob(["*.toml"])
13+
)

tools/fpga_releaser/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import importlib.resources as resources
2+
3+
files = resources.files(__name__)
4+
config_toml = files / "config.toml"

tools/fpga_releaser/archive_parser.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# We're going to be handed a zip archive of the buck2 build
2+
# It contains a directory with some hash name
3+
# For a yosys build we're going to need the following things:
4+
# the bz2 file
5+
# the nextpnr log at that same directory called nextpnr.log
6+
# all the stuff in the _maps_ directory at that same level
7+
8+
def get_relevant_files_from_buck_zip(zip):
9+
zip_names = []
10+
for item in zip.infolist():
11+
if item.filename.endswith(".bz2"):
12+
zip_names.append(item.filename)
13+
if "/maps/" in item.filename and (item.filename.endswith(".json") or item.filename.endswith(".html")):
14+
zip_names.append(item.filename)
15+
if item.filename.endswith("nextpnr.log"):
16+
zip_names.append(item.filename)
17+
# Vivado stuff
18+
if item.filename.endswith("route_timing.rpt"):
19+
zip_names.append(item.filename)
20+
if item.filename.endswith("place_optimize_utilization.rpt"):
21+
zip_names.append(item.filename)
22+
if item.filename.endswith("synthesize.log"):
23+
zip_names.append(item.filename)
24+
if item.filename.endswith("route.log"):
25+
zip_names.append(item.filename)
26+
if item.filename.endswith("place.log"):
27+
zip_names.append(item.filename)
28+
if item.filename.endswith("optimize.log"):
29+
zip_names.append(item.filename)
30+
31+
return zip_names

tools/fpga_releaser/cli.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import sys
2+
import os
3+
import io
4+
import argparse
5+
import zipfile
6+
7+
from pathlib import Path
8+
import arrow
9+
import requests
10+
from ghapi.all import GhApi
11+
from fastcore.xtras import obj2dict
12+
13+
import fpga_releaser.project as project
14+
import fpga_releaser.gh_releaser as gh_releaser
15+
16+
17+
18+
parser = argparse.ArgumentParser()
19+
parser.add_argument("--token", default=None, help="Pass in your GitHub personal access token (will also use from GH_ACCESS_TOKEN env variable")
20+
parser.add_argument("--branch", default="main", help="Quartz branch to use for the artifact")
21+
parser.add_argument("--fpga", help="Name of FPGA project to release")
22+
parser.add_argument("--hubris", default=None, help="Path to hubris git checkout as target for copying files. Will skip if None")
23+
parser.add_argument("--skip-gh", default=False, action="store_true", help="Skip doing GH release. Note that doing this still generates release metadata that just will be wrong")
24+
parser.add_argument("--zip", default=None, help="Path to zip file to use instead of downloading from GitHub")
25+
26+
hubris_ignore = [".html", ".log", ".rpt"]
27+
28+
def main():
29+
"""
30+
Main function to process the command line arguments and run the release process.
31+
"""
32+
args = parser.parse_args()
33+
if args.fpga is None:
34+
print("Please specify an FPGA project with --fpga")
35+
sys.exit(1)
36+
37+
if args.token is not None:
38+
token = args.token
39+
else:
40+
token = os.getenv("GITHUB_TOKEN", None)
41+
if token is None:
42+
print("the --token option or an env_variable GITHUB_TOKEN is required to use this tool")
43+
sys.exit(1)
44+
45+
repo = "quartz"
46+
api = GhApi(owner='oxidecomputer', repo=repo, token=token)
47+
48+
project_info = project.get_config(args.fpga)
49+
print(f"Processing {args.fpga} with builder {project_info.builder}")
50+
51+
# Get build archive
52+
zip_file = process_gh_build(args, api, project_info.job_name)
53+
project_info.add_archive(zip_file)
54+
55+
# Do build reports
56+
project_info.report_timing()
57+
project_info.report_utilization()
58+
59+
# extract files for GH release
60+
# do GH release
61+
if args.skip_gh:
62+
print("Skipping GH release per command line request")
63+
gh_releaser.do_gh_release(api, project_info, args.skip_gh)
64+
65+
# put files in hubris location
66+
hubris_path = Path(args.hubris) / Path(project_info.hubris_path)
67+
print(f"Materializing files at {hubris_path} for hubris commit")
68+
project_info.materialize_relevant_files(hubris_path, exclusions=hubris_ignore)
69+
70+
71+
72+
def process_gh_build(args, api, name: str):
73+
74+
# Get build archive zip file
75+
# Get the latest artifact from the repo since we didn't specify a zip file
76+
if args.zip is None:
77+
print("Fetching latest from GitHub")
78+
# Download the artifact from github
79+
artifact_inf = get_latest_artifact_info(api, name, branch=args.branch)
80+
zip_file = download_artifact(api, artifact_inf)
81+
else:
82+
print("Using local zip file")
83+
# Use the zip file from the command line
84+
zip_file = zipfile.ZipFile(args.zip)
85+
86+
return zip_file
87+
88+
89+
def get_latest_artifact_info(api, fpga_name: str, branch: str = "main") -> dict:
90+
"""
91+
Get the latest artifact from the specified branch.
92+
"""
93+
artifacts = api.actions.list_artifacts_for_repo(name=fpga_name)
94+
artifacts = obj2dict(artifacts)
95+
artifacts = list(filter(lambda x: x["workflow_run"]["head_branch"] == branch, artifacts["artifacts"]))
96+
if len(artifacts) == 0:
97+
print(f"No artifacts found for {fpga_name} on {branch}")
98+
return None
99+
artifacts = sorted(artifacts, key=lambda x: arrow.get(x["created_at"]), reverse=True)
100+
return artifacts[0]
101+
102+
def download_artifact(api: GhApi, artifact_inf: dict):
103+
print(f"Downloading artifact {artifact_inf['name']} from GH: {artifact_inf['workflow_run']['head_branch']}")
104+
r = requests.get(artifact_inf["archive_download_url"], auth=("oxidecomputer", os.getenv("GITHUB_TOKEN", None)))
105+
return zipfile.ZipFile(io.BytesIO(r.content))
106+
107+
if __name__ == '__main__':
108+
main()
109+
110+

tools/fpga_releaser/config.toml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[cosmo-hp]
2+
job_name = "cosmo-hp-image"
3+
hubris_path = "drv/ice40-loader/cosmo-hp"
4+
builder = "buck2"
5+
toolchain = "yosys"
6+
7+
[cosmo-seq]
8+
job_name = "cosmo-seq-image"
9+
hubris_path = "drv/spartan7-loader/cosmo-seq"
10+
builder = "buck2"
11+
toolchain = "vivado"
12+
13+
[grapefruit]
14+
job_name = "gfruit-image"
15+
hubris_path = "drv/spartan7-loader/grapefruit"
16+
builder = "buck2"
17+
toolchain = "vivado"

tools/fpga_releaser/gh_releaser.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# create a release with image-name-TIMESTAMP?
2+
# push up logs, binary, docs and readme
3+
from ghapi.all import GhApi
4+
from pathlib import Path
5+
import tempfile
6+
7+
8+
def do_gh_release(api: GhApi, info, skip_mods=False):
9+
10+
name = f"{info.name}-{info.timestamp}"
11+
body = f"FPGA release for {info.name} made on {info.timestamp} UTC"
12+
# TODO: should we add something about timing failure here?
13+
# TODO: info about this being a locally generated file?
14+
tag_name = name
15+
if info.gh_build_sha == "":
16+
# Built locally? Or not from a GH release
17+
sha = api.git.get_ref("heads/main")["object"].get("sha")
18+
else:
19+
sha = info.gh_build_sha
20+
21+
info.add_build_sha(sha) # in case local build
22+
23+
# Create a tag
24+
if not skip_mods:
25+
api.git.create_tag(tag_name, body, sha, "commit")
26+
27+
# Stick the release files somewhere temporary
28+
with tempfile.TemporaryDirectory() as temp_dir:
29+
files = info.materialize_relevant_files(temp_dir)
30+
full_files = [Path(temp_dir) / Path(x) for x in files]
31+
# Create a Release
32+
if not skip_mods:
33+
rel = api.create_release(tag_name,sha,name,body, files=full_files)
34+
info.add_release_url(rel.get('url'))
35+
else:
36+
info.add_release_url('None: gh release was skipped')
37+

0 commit comments

Comments
 (0)