Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated the developement status of apio from Alpha to Production/Stable #441

Merged
merged 4 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apio/managers/scons.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def wrapper(*args, **kwargs):
try:
return function(*args, **kwargs)
except Exception as exc:
# For debugging. Uncomment to print the exception's stack.
# import traceback
# traceback.print_tb(exc.__traceback__)

if str(exc):
click.secho("Error: " + str(exc), fg="red")
return exit_code
Expand Down
132 changes: 98 additions & 34 deletions apio/managers/scons_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ class RangeEvents(Enum):
END_AFTER = 4 # Range ends, after the current line.


class SectionDetector:
"""Base classifier of a range of lines within the sequence of stdout/err
class RangeDetector:
"""Base detector of a range of lines within the sequence of stdout/err
lines recieves from the scons subprocess."""

def __init__(self):
self._in_range = False

def update(self, pipe_id: PipeId, line: str) -> bool:
"""Updates the section classifier with the next stdout/err line.
"""Updates the range detector with the next stdout/err line.
return True iff detector classified this line to be within a range."""

prev_state = self._in_range
Expand Down Expand Up @@ -85,11 +85,12 @@ def classify_line(
raise NotImplementedError("Should be implemented by a subclass")


class PnrSectionDetector(SectionDetector):
"""Implements a RangeDetector for the nextpnr command verbose log lines."""
class PnrRangeDetector(RangeDetector):
"""Implements a RangeDetector for the nextpnr command verbose
log lines."""

def classify_line(self, pipe_id: PipeId, line: str) -> RangeEvents:
# -- Brek line into words.
# -- Break line into words.
tokens = line.split()

# -- Range start: A nextpnr command on stdout without
Expand All @@ -108,14 +109,38 @@ def classify_line(self, pipe_id: PipeId, line: str) -> RangeEvents:
return None


class IceProgRangeDetector(RangeDetector):
"""Implements a RangeDetector for the iceprog command output."""

def __init__(self):
super().__init__()
# -- Indicates if the last line should be erased before printing the
# -- next one. This happens with interactive progress meters.
self.pending_erasure = False

def classify_line(self, pipe_id: PipeId, line: str) -> RangeEvents:
# -- Range start: A nextpnr command on stdout without
# -- the -q (quiet) flag.
if pipe_id == PipeId.STDOUT and line.startswith("iceprog"):
self.pending_erasure = False
return RangeEvents.START_AFTER

# Range end: The end message of nextnpr.
if pipe_id == PipeId.STDERR and line.startswith("Bye."):
return RangeEvents.END_AFTER

return None


class SconsFilter:
"""Implements the filtering and printing of the stdout/err streams of the
scons subprocess. Accepts a line one at a time, detects lines ranges of
intereset, mutates and colors the lines where applicable, and print to
stdout."""

def __init__(self):
self._pnr_detector = PnrSectionDetector()
self._pnr_detector = PnrRangeDetector()
self._iceprog_detector = IceProgRangeDetector()

def on_stdout_line(self, line: str) -> None:
"""Stdout pipe calls this on each line."""
Expand Down Expand Up @@ -150,8 +175,9 @@ def on_line(self, pipe_id: PipeId, line: str) -> None:
from other programs. See the PNR detector for an example.
"""

# -- Update the classifiers
# -- Update the range detectors.
in_pnr_verbose_range = self._pnr_detector.update(pipe_id, line)
in_iceprog_range = self._iceprog_detector.update(pipe_id, line)

# -- Handle the line while in the nextpnr verbose log range.
if pipe_id == PipeId.STDERR and in_pnr_verbose_range:
Expand All @@ -164,22 +190,75 @@ def on_line(self, pipe_id: PipeId, line: str) -> None:
# -- Assign line color.
line_color = self._assign_line_color(
line.lower(),
{
[
(r"^warning:", "yellow"),
(r"^error:", "red"),
},
],
)
click.secho(f"{line}", fg=line_color)
return

# -- Special handling for iceprog line range.
if pipe_id == PipeId.STDERR and in_iceprog_range:
# -- Iceprog prints blank likes that are used as line erasers.
# -- We don't need them here.
if len(line) == 0:
return

# -- If the last iceprog line was a to-be-erased line, erase it
# -- now and clear the flag.
if self._iceprog_detector.pending_erasure:
print(
CURSOR_UP + ERASE_LINE,
end="",
flush=True,
)
self._iceprog_detector.pending_erasure = False

# -- Determine if the current line should be erased before we will
# -- print the next line.
# --
# -- Match outputs like these "addr 0x001400 3%"
# -- Regular expression remainder:
# -- ^ --> Match the begining of the line
# -- \s --> Match one blank space
# -- [0-9A-F]+ one or more hexadecimal digit
# -- \d{1,2} one or two decimal digits
pattern = r"^addr\s0x[0-9A-F]+\s+\d{1,2}%"

# -- Calculate if there is a match!
match = re.search(pattern, line)

# -- If the line is to be erased set the flag.
if match:
self._iceprog_detector.pending_erasure = True

# -- Determine line color by its content and print it.
line_color = self._assign_line_color(
line,
[
(r"^done.", "green"),
(r"^VERIFY OK", "green"),
],
)
click.secho(line, fg=line_color)
return

# -- Special handling for Fumo lines.
if pipe_id == PipeId.STDOUT:
pattern_fomu = r"^Download\s*\[=*"
match = re.search(pattern_fomu, line)
if match:
# -- Delete the previous line
#
# -- NOTE: If the progress line will scroll instead of
# -- overwriting each other, try to add erasure of a second
# -- line. This is due to the commit below which restored
# -- empty lines.
# - Commit 93fc9bc4f3bfd21568e2d66f11976831467e3b97.
#
print(CURSOR_UP + ERASE_LINE, end="", flush=True)
click.secho(f"{line}", fg="green")
click.secho(line, fg="green")
return

# -- Special handling for tinyprog lines.
Expand All @@ -198,29 +277,14 @@ def on_line(self, pipe_id: PipeId, line: str) -> None:
# -- Match all the progress bar lines except the
# -- initial one (when it is 0%)
if match_tinyprog and " 0%|" not in line:
# -- Delete the previous line
print(CURSOR_UP + ERASE_LINE, end="", flush=True)
click.secho(f"{line}")
return

# -- Special handling for iceprog lines.
if pipe_id == PipeId.STDERR:
# -- Match outputs like these "addr 0x001400 3%"
# -- Regular expression remainder:
# -- ^ --> Match the begining of the line
# -- \s --> Match one blank space
# -- [0-9A-F]+ one or more hexadecimal digit
# -- \d{1,2} one or two decimal digits
pattern = r"^addr\s0x[0-9A-F]+\s+\d{1,2}%"

# -- Calculate if there is a match!
match = re.search(pattern, line)

# -- It is a match! (iceprog is running!)
# -- (or if it is the end of the writing!)
# -- (or if it is the end of verifying!)
if match or "done." in line or "VERIFY OK" in line:
# -- Delete the previous line
# -- Delete the previous line.
#
# -- NOTE: If the progress line will scroll instead of
# -- overwriting each other, try to add erasure of a second
# -- line. This is due to the commit below which restored
# -- empty lines.
# - Commit 93fc9bc4f3bfd21568e2d66f11976831467e3b97.
#
print(CURSOR_UP + ERASE_LINE, end="", flush=True)
click.secho(line)
return
Expand Down
4 changes: 2 additions & 2 deletions apio/managers/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def get_usb_devices(self) -> list:
# -- Get the list of the usb devices. It is read
# -- from the command stdout
# -- Ex: [{'hwid':'1d6b:0003'}, {'hwid':'04f2:b68b'}...]
usb_devices = self._parse_usb_devices(result["out"])
usb_devices = self._parse_usb_devices(result.out_text)

# -- Return the devices
return usb_devices
Expand Down Expand Up @@ -130,7 +130,7 @@ def get_ftdi_devices(self) -> list:
# -- from the command stdout
# -- Ex: [{'index': '0', 'manufacturer': 'AlhambraBits',
# -- 'description': 'Alhambra II v1.0A - B07-095'}]
ftdi_devices = self._parse_ftdi_devices(result["out"])
ftdi_devices = self._parse_ftdi_devices(result.out_text)

# -- Return the devices
return ftdi_devices
Expand Down
4 changes: 2 additions & 2 deletions apio/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,14 +686,14 @@ def exec_command(*args, **kwargs) -> CommandResult:
if isinstance(pipe, AsyncPipe):
lines = pipe.get_buffer()
text = "\n".join(lines)
out_text = text.strip()
out_text = text

# -- If stderr pipe is an AsyncPipe, extract its text.
pipe = flags["stderr"]
if isinstance(pipe, AsyncPipe):
lines = pipe.get_buffer()
text = "\n".join(lines)
err_text = text.strip()
err_text = text

# -- All done.
result = CommandResult(out_text, err_text, exit_code)
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ author = "Jesus Arroyo"
author-email = "jesus.jkhlg@gmail.com "
home-page = "https://github.com/FPGAwars/apio"
classifiers=[
'Development Status :: 3 - Alpha',
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
'Programming Language :: Python']
'Programming Language :: Python',
'Natural Language :: English',
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
]
description-file = "README.md"
requires-python = ">=3.9"
requires = [
Expand Down