Pcileech/src/build.py

2206 lines
87 KiB
Python

#!/usr/bin/env python3
"""
PCILeech FPGA Firmware Builder - Production System
This is a complete, production-level build system for generating PCILeech DMA firmware
for various FPGA boards using donor device configuration space information obtained via VFIO.
Features:
- VFIO-based configuration space extraction
- Advanced SystemVerilog generation
- Manufacturing variance simulation
- Device-specific optimizations
- Behavior profiling
- MSI-X capability handling
- Option ROM management
- Configuration space shadowing
Usage:
python3 build.py --bdf 0000:03:00.0 --board pcileech_35t325_x4
Boards:
pcileech_35t325_x4 → Artix-7 35T (PCIeSquirrel)
pcileech_75t → Kintex-7 75T (PCIeEnigmaX1)
pcileech_100t → Zynq UltraScale+ (XilinxZDMA)
"""
import argparse
import json
import logging
import os
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
# Import project modules
try:
from behavior_profiler import BehaviorProfiler
from donor_dump_manager import DonorDumpError, DonorDumpManager
from manufacturing_variance import (
DeviceClass,
ManufacturingVarianceSimulator,
VarianceModel,
)
from vivado_utils import find_vivado_installation
except ImportError as e:
# Try relative imports for container environment
try:
from .behavior_profiler import BehaviorProfiler
from .donor_dump_manager import DonorDumpError, DonorDumpManager
from .manufacturing_variance import (
DeviceClass,
ManufacturingVarianceSimulator,
VarianceModel,
)
from .vivado_utils import find_vivado_installation
except ImportError:
print(f"Error importing required modules: {e}")
print("Falling back to basic functionality...")
DonorDumpManager = None
ManufacturingVarianceSimulator = None
DeviceClass = None
VarianceModel = None
BehaviorProfiler = None
find_vivado_installation = None
# Try to import advanced modules (optional)
try:
from advanced_sv_generator import AdvancedSystemVerilogGenerator
except ImportError:
try:
from .advanced_sv_generator import AdvancedSystemVerilogGenerator
except ImportError:
AdvancedSystemVerilogGenerator = None
try:
from msix_capability import MSIXCapabilityManager
except ImportError:
try:
from .msix_capability import MSIXCapabilityManager
except ImportError:
MSIXCapabilityManager = None
try:
from option_rom_manager import OptionROMManager
except ImportError:
try:
from .option_rom_manager import OptionROMManager
except ImportError:
OptionROMManager = None
# Set up logging
def setup_logging(output_dir: Optional[Path] = None):
"""Set up logging with appropriate handlers."""
handlers = [logging.StreamHandler(sys.stdout)]
# Add file handler if output directory exists
if output_dir and output_dir.exists():
log_file = output_dir / "build.log"
handlers.append(logging.FileHandler(str(log_file), mode="a"))
elif os.path.exists("/app/output"):
# Container environment
handlers.append(logging.FileHandler("/app/output/build.log", mode="a"))
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=handlers,
force=True, # Override any existing configuration
)
# Initialize basic logging (will be reconfigured in main)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger(__name__)
class PCILeechFirmwareBuilder:
"""Main firmware builder class."""
def __init__(self, bdf: str, board: str, output_dir: Optional[Path] = None):
self.bdf = bdf
self.board = board
# Set output directory based on environment
if output_dir:
self.output_dir = output_dir
elif os.path.exists("/app/output"):
self.output_dir = Path("/app/output")
else:
self.output_dir = Path("./output")
self.output_dir.mkdir(exist_ok=True)
# Reconfigure logging with proper output directory
setup_logging(self.output_dir)
# Initialize components
self.donor_manager = DonorDumpManager() if DonorDumpManager else None
self.sv_generator = None
self.variance_simulator = (
ManufacturingVarianceSimulator() if ManufacturingVarianceSimulator else None
)
self.behavior_profiler = None
self.msix_manager = MSIXCapabilityManager() if MSIXCapabilityManager else None
self.option_rom_manager = OptionROMManager() if OptionROMManager else None
logger.info(f"Initialized PCILeech firmware builder for {bdf} on {board}")
def read_vfio_config_space(self) -> bytes:
"""Read PCI configuration space via VFIO."""
try:
# Find IOMMU group for the device
iommu_group_path = f"/sys/bus/pci/devices/{self.bdf}/iommu_group"
if not os.path.exists(iommu_group_path):
raise RuntimeError(f"IOMMU group not found for device {self.bdf}")
iommu_group = os.path.basename(os.readlink(iommu_group_path))
vfio_device = f"/dev/vfio/{iommu_group}"
if not os.path.exists(vfio_device):
raise RuntimeError(f"VFIO device {vfio_device} not found")
logger.info(
f"Reading configuration space for device {self.bdf} via VFIO group {iommu_group}"
)
# Read actual configuration space from sysfs as fallback
config_path = f"/sys/bus/pci/devices/{self.bdf}/config"
if os.path.exists(config_path):
with open(config_path, "rb") as f:
config_space = f.read(256) # Read first 256 bytes
logger.info(
f"Successfully read {len(config_space)} bytes of configuration space"
)
return config_space
else:
# Generate synthetic configuration space if real one not available
logger.warning(
"Real config space not available, generating synthetic data"
)
return self._generate_synthetic_config_space()
except Exception as e:
logger.error(f"Failed to read VFIO config space: {e}")
logger.info("Generating synthetic configuration space as fallback")
return self._generate_synthetic_config_space()
def _generate_synthetic_config_space(self) -> bytes:
"""Generate production-quality synthetic PCI configuration space with realistic device profiles."""
config_space = bytearray(4096) # Extended config space (4KB)
# Determine device profile based on BDF or use intelligent defaults
device_profiles = {
# Network controllers
"network": {
"vendor_id": 0x8086, "device_id": 0x125c, "class_code": 0x020000,
"subsys_vendor": 0x8086, "subsys_device": 0x0000,
"bar_configs": [0xf0000000, 0x00000000, 0xf0010000, 0x00000000, 0x0000e001, 0x00000000],
"capabilities": ["msi", "msix", "pcie", "pm"]
},
# Storage controllers
"storage": {
"vendor_id": 0x1b4b, "device_id": 0x9230, "class_code": 0x010802,
"subsys_vendor": 0x1b4b, "subsys_device": 0x9230,
"bar_configs": [0xf0000000, 0x00000000, 0x0000e001, 0x00000000, 0x00000000, 0x00000000],
"capabilities": ["msi", "msix", "pcie", "pm"]
},
# Audio controllers
"audio": {
"vendor_id": 0x8086, "device_id": 0x9dc8, "class_code": 0x040300,
"subsys_vendor": 0x8086, "subsys_device": 0x7270,
"bar_configs": [0xf0000000, 0x00000000, 0x0000e001, 0x00000000, 0x00000000, 0x00000000],
"capabilities": ["msi", "pcie", "pm"]
}
}
# Select profile based on device characteristics or default to network
profile = device_profiles["network"] # Default to most common PCILeech target
# Standard PCI Configuration Header (0x00-0x3F)
# Vendor ID and Device ID
config_space[0:2] = profile["vendor_id"].to_bytes(2, 'little')
config_space[2:4] = profile["device_id"].to_bytes(2, 'little')
# Command Register - Enable memory space, bus master, disable I/O space
config_space[4:6] = (0x0006).to_bytes(2, 'little') # Memory Space + Bus Master
# Status Register - Capabilities list, 66MHz capable, fast back-to-back
config_space[6:8] = (0x0210).to_bytes(2, 'little') # Cap List + Fast B2B
# Revision ID and Class Code
config_space[8] = 0x04 # Revision ID
config_space[9] = (profile["class_code"] & 0xFF) # Programming Interface
config_space[10:12] = ((profile["class_code"] >> 8) & 0xFFFF).to_bytes(2, 'little')
# Cache Line Size, Latency Timer, Header Type, BIST
config_space[12] = 0x10 # Cache line size (16 bytes)
config_space[13] = 0x00 # Latency timer
config_space[14] = 0x00 # Single function device
config_space[15] = 0x00 # BIST not supported
# Base Address Registers (BARs)
for i, bar_val in enumerate(profile["bar_configs"]):
offset = 16 + (i * 4)
config_space[offset:offset+4] = bar_val.to_bytes(4, 'little')
# Cardbus CIS Pointer (unused)
config_space[40:44] = (0x00000000).to_bytes(4, 'little')
# Subsystem Vendor ID and Subsystem ID
config_space[44:46] = profile["subsys_vendor"].to_bytes(2, 'little')
config_space[46:48] = profile["subsys_device"].to_bytes(2, 'little')
# Expansion ROM Base Address (disabled)
config_space[48:52] = (0x00000000).to_bytes(4, 'little')
# Capabilities Pointer
config_space[52] = 0x40 # First capability at 0x40
# Reserved fields
config_space[53:60] = b'\x00' * 7
# Interrupt Line, Interrupt Pin, Min_Gnt, Max_Lat
config_space[60] = 0xFF # Interrupt line (not connected)
config_space[61] = 0x01 # Interrupt pin A
config_space[62] = 0x00 # Min_Gnt
config_space[63] = 0x00 # Max_Lat
# Build capability chain starting at 0x40
cap_offset = 0x40
# Power Management Capability (always present)
if "pm" in profile["capabilities"]:
config_space[cap_offset] = 0x01 # PM Capability ID
config_space[cap_offset + 1] = 0x50 # Next capability pointer
config_space[cap_offset + 2:cap_offset + 4] = (0x0003).to_bytes(2, 'little') # PM Capabilities
config_space[cap_offset + 4:cap_offset + 6] = (0x0000).to_bytes(2, 'little') # PM Control/Status
cap_offset = 0x50
# MSI Capability
if "msi" in profile["capabilities"]:
config_space[cap_offset] = 0x05 # MSI Capability ID
config_space[cap_offset + 1] = 0x60 # Next capability pointer
config_space[cap_offset + 2:cap_offset + 4] = (0x0080).to_bytes(2, 'little') # MSI Control (64-bit)
config_space[cap_offset + 4:cap_offset + 8] = (0x00000000).to_bytes(4, 'little') # Message Address
config_space[cap_offset + 8:cap_offset + 12] = (0x00000000).to_bytes(4, 'little') # Message Upper Address
config_space[cap_offset + 12:cap_offset + 14] = (0x0000).to_bytes(2, 'little') # Message Data
cap_offset = 0x60
# MSI-X Capability
if "msix" in profile["capabilities"]:
config_space[cap_offset] = 0x11 # MSI-X Capability ID
config_space[cap_offset + 1] = 0x70 # Next capability pointer
config_space[cap_offset + 2:cap_offset + 4] = (0x0000).to_bytes(2, 'little') # MSI-X Control
config_space[cap_offset + 4:cap_offset + 8] = (0x00000000).to_bytes(4, 'little') # Table Offset/BIR
config_space[cap_offset + 8:cap_offset + 12] = (0x00002000).to_bytes(4, 'little') # PBA Offset/BIR
cap_offset = 0x70
# PCIe Capability (for modern devices)
if "pcie" in profile["capabilities"]:
config_space[cap_offset] = 0x10 # PCIe Capability ID
config_space[cap_offset + 1] = 0x00 # Next capability pointer (end of chain)
config_space[cap_offset + 2:cap_offset + 4] = (0x0002).to_bytes(2, 'little') # PCIe Capabilities
config_space[cap_offset + 4:cap_offset + 8] = (0x00000000).to_bytes(4, 'little') # Device Capabilities
config_space[cap_offset + 8:cap_offset + 10] = (0x0000).to_bytes(2, 'little') # Device Control
config_space[cap_offset + 10:cap_offset + 12] = (0x0000).to_bytes(2, 'little') # Device Status
config_space[cap_offset + 12:cap_offset + 16] = (0x00000000).to_bytes(4, 'little') # Link Capabilities
config_space[cap_offset + 16:cap_offset + 18] = (0x0000).to_bytes(2, 'little') # Link Control
config_space[cap_offset + 18:cap_offset + 20] = (0x0000).to_bytes(2, 'little') # Link Status
logger.info(f"Generated synthetic config space: VID={profile['vendor_id']:04x}, DID={profile['device_id']:04x}, Class={profile['class_code']:06x}")
return bytes(config_space[:256]) # Return standard 256-byte config space
def extract_device_info(self, config_space: bytes) -> Dict[str, Any]:
"""Extract device information from configuration space."""
if len(config_space) < 64:
raise ValueError("Configuration space too short")
vendor_id = int.from_bytes(config_space[0:2], "little")
device_id = int.from_bytes(config_space[2:4], "little")
class_code = int.from_bytes(config_space[10:12], "little")
revision_id = config_space[8]
# Extract BARs
bars = []
for i in range(6):
bar_offset = 16 + (i * 4)
if bar_offset + 4 <= len(config_space):
bar_value = int.from_bytes(
config_space[bar_offset : bar_offset + 4], "little"
)
bars.append(bar_value)
device_info = {
"vendor_id": f"{vendor_id:04x}",
"device_id": f"{device_id:04x}",
"class_code": f"{class_code:04x}",
"revision_id": f"{revision_id:02x}",
"bdf": self.bdf,
"board": self.board,
"bars": bars,
"config_space_hex": config_space.hex(),
"config_space_size": len(config_space),
}
logger.info(
f"Extracted device info: VID={device_info['vendor_id']}, DID={device_info['device_id']}"
)
return device_info
def generate_systemverilog_files(
self,
device_info: Dict[str, Any],
advanced_sv: bool = False,
device_type: Optional[str] = None,
enable_variance: bool = False,
) -> List[str]:
"""Generate SystemVerilog files for the firmware."""
generated_files = []
try:
# Initialize advanced SystemVerilog generator if available and requested
if advanced_sv and AdvancedSystemVerilogGenerator:
logger.info("Generating advanced SystemVerilog modules")
self.sv_generator = AdvancedSystemVerilogGenerator()
# Generate device-specific modules
if device_type:
device_modules = self.sv_generator.generate_device_specific_modules(
device_type, device_info
)
for module_name, module_content in device_modules.items():
file_path = self.output_dir / f"{module_name}.sv"
with open(file_path, "w") as f:
f.write(module_content)
generated_files.append(str(file_path))
logger.info(f"Generated advanced SV module: {module_name}.sv")
# Discover and copy all relevant project files
project_files = self._discover_and_copy_all_files(device_info)
generated_files.extend(project_files)
# Generate manufacturing variance if enabled
if enable_variance and ManufacturingVarianceSimulator:
logger.info("Applying manufacturing variance simulation")
self.variance_simulator = ManufacturingVarianceSimulator()
variance_files = self._apply_manufacturing_variance(device_info)
generated_files.extend(variance_files)
except Exception as e:
logger.error(f"Error generating SystemVerilog files: {e}")
raise
return generated_files
def _discover_and_copy_all_files(self, device_info: Dict[str, Any]) -> List[str]:
"""Scalable discovery and copying of all relevant project files."""
copied_files = []
src_dir = Path(__file__).parent
# Discover all SystemVerilog files (including subdirectories)
sv_files = list(src_dir.rglob("*.sv"))
logger.info(f"Discovered {len(sv_files)} SystemVerilog files")
# Validate and copy SystemVerilog modules
valid_sv_files = []
for sv_file in sv_files:
try:
with open(sv_file, "r") as f:
content = f.read()
# Basic validation - check for module declaration
if "module " in content and "endmodule" in content:
dest_path = self.output_dir / sv_file.name
with open(dest_path, "w") as dest:
dest.write(content)
copied_files.append(str(dest_path))
valid_sv_files.append(sv_file.name)
logger.info(f"Copied valid SystemVerilog module: {sv_file.name}")
else:
logger.warning(f"Skipping invalid SystemVerilog file: {sv_file.name}")
except Exception as e:
logger.error(f"Error processing {sv_file.name}: {e}")
# Discover and copy all TCL files (preserve as-is)
tcl_files = list(src_dir.rglob("*.tcl"))
for tcl_file in tcl_files:
try:
dest_path = self.output_dir / tcl_file.name
with open(tcl_file, "r") as src, open(dest_path, "w") as dest:
content = src.read()
dest.write(content)
copied_files.append(str(dest_path))
logger.info(f"Copied TCL script: {tcl_file.name}")
except Exception as e:
logger.error(f"Error copying TCL file {tcl_file.name}: {e}")
# Discover and copy constraint files
xdc_files = list(src_dir.rglob("*.xdc"))
for xdc_file in xdc_files:
try:
dest_path = self.output_dir / xdc_file.name
with open(xdc_file, "r") as src, open(dest_path, "w") as dest:
content = src.read()
dest.write(content)
copied_files.append(str(dest_path))
logger.info(f"Copied constraint file: {xdc_file.name}")
except Exception as e:
logger.error(f"Error copying constraint file {xdc_file.name}: {e}")
# Discover and copy any Verilog files
v_files = list(src_dir.rglob("*.v"))
for v_file in v_files:
try:
dest_path = self.output_dir / v_file.name
with open(v_file, "r") as src, open(dest_path, "w") as dest:
content = src.read()
dest.write(content)
copied_files.append(str(dest_path))
logger.info(f"Copied Verilog module: {v_file.name}")
except Exception as e:
logger.error(f"Error copying Verilog file {v_file.name}: {e}")
# Generate device-specific configuration module
config_module = self._generate_device_config_module(device_info)
config_path = self.output_dir / "device_config.sv"
with open(config_path, "w") as f:
f.write(config_module)
copied_files.append(str(config_path))
# Generate top-level wrapper
top_module = self._generate_top_level_wrapper(device_info)
top_path = self.output_dir / "pcileech_top.sv"
with open(top_path, "w") as f:
f.write(top_module)
copied_files.append(str(top_path))
return copied_files
def _generate_device_config_module(self, device_info: Dict[str, Any]) -> str:
"""Generate device-specific configuration module using actual device data."""
vendor_id = device_info["vendor_id"]
device_id = device_info["device_id"]
class_code = device_info["class_code"]
revision_id = device_info["revision_id"]
bars = device_info["bars"]
return f"""
//==============================================================================
// Device Configuration Module - Generated for {vendor_id}:{device_id}
// Board: {device_info['board']}
//==============================================================================
module device_config #(
parameter VENDOR_ID = 16'h{vendor_id},
parameter DEVICE_ID = 16'h{device_id},
parameter CLASS_CODE = 24'h{class_code}{revision_id},
parameter SUBSYSTEM_VENDOR_ID = 16'h{vendor_id},
parameter SUBSYSTEM_DEVICE_ID = 16'h{device_id},
parameter BAR0_APERTURE = 32'h{bars[0]:08x},
parameter BAR1_APERTURE = 32'h{bars[1]:08x},
parameter BAR2_APERTURE = 32'h{bars[2]:08x},
parameter BAR3_APERTURE = 32'h{bars[3]:08x},
parameter BAR4_APERTURE = 32'h{bars[4]:08x},
parameter BAR5_APERTURE = 32'h{bars[5]:08x}
) (
// Configuration space interface
output logic [31:0] cfg_device_id,
output logic [31:0] cfg_class_code,
output logic [31:0] cfg_subsystem_id,
output logic [31:0] cfg_bar [0:5]
);
// Device identification
assign cfg_device_id = {{DEVICE_ID, VENDOR_ID}};
assign cfg_class_code = {{8'h00, CLASS_CODE}};
assign cfg_subsystem_id = {{SUBSYSTEM_DEVICE_ID, SUBSYSTEM_VENDOR_ID}};
// BAR configuration
assign cfg_bar[0] = BAR0_APERTURE;
assign cfg_bar[1] = BAR1_APERTURE;
assign cfg_bar[2] = BAR2_APERTURE;
assign cfg_bar[3] = BAR3_APERTURE;
assign cfg_bar[4] = BAR4_APERTURE;
assign cfg_bar[5] = BAR5_APERTURE;
endmodule
"""
def _generate_top_level_wrapper(self, device_info: Dict[str, Any]) -> str:
"""Generate top-level wrapper that integrates all modules."""
return f"""
//==============================================================================
// PCILeech Top-Level Wrapper - Generated for {device_info['vendor_id']}:{device_info['device_id']}
// Board: {self.board}
//==============================================================================
module pcileech_top (
// Clock and reset
input logic clk,
input logic reset_n,
// PCIe interface (connect to PCIe hard IP)
input logic [31:0] pcie_rx_data,
input logic pcie_rx_valid,
output logic [31:0] pcie_tx_data,
output logic pcie_tx_valid,
// Configuration space interface
input logic cfg_ext_read_received,
input logic cfg_ext_write_received,
input logic [9:0] cfg_ext_register_number,
input logic [3:0] cfg_ext_function_number,
input logic [31:0] cfg_ext_write_data,
input logic [3:0] cfg_ext_write_byte_enable,
output logic [31:0] cfg_ext_read_data,
output logic cfg_ext_read_data_valid,
// MSI-X interrupt interface
output logic msix_interrupt,
output logic [10:0] msix_vector,
input logic msix_interrupt_ack,
// Debug/status outputs
output logic [31:0] debug_status,
output logic device_ready
);
// Internal signals
logic [31:0] bar_addr;
logic [31:0] bar_wr_data;
logic bar_wr_en;
logic bar_rd_en;
logic [31:0] bar_rd_data;
// Device configuration signals
logic [31:0] cfg_device_id;
logic [31:0] cfg_class_code;
logic [31:0] cfg_subsystem_id;
logic [31:0] cfg_bar [0:5];
// Instantiate device configuration
device_config device_cfg (
.cfg_device_id(cfg_device_id),
.cfg_class_code(cfg_class_code),
.cfg_subsystem_id(cfg_subsystem_id),
.cfg_bar(cfg_bar)
);
// Instantiate BAR controller
pcileech_tlps128_bar_controller #(
.BAR_APERTURE_SIZE(131072), // 128KB
.NUM_MSIX(1),
.MSIX_TABLE_BIR(0),
.MSIX_TABLE_OFFSET(0),
.MSIX_PBA_BIR(0),
.MSIX_PBA_OFFSET(0)
) bar_controller (
.clk(clk),
.reset_n(reset_n),
.bar_addr(bar_addr),
.bar_wr_data(bar_wr_data),
.bar_wr_en(bar_wr_en),
.bar_rd_en(bar_rd_en),
.bar_rd_data(bar_rd_data),
.cfg_ext_read_received(cfg_ext_read_received),
.cfg_ext_write_received(cfg_ext_write_received),
.cfg_ext_register_number(cfg_ext_register_number),
.cfg_ext_function_number(cfg_ext_function_number),
.cfg_ext_write_data(cfg_ext_write_data),
.cfg_ext_write_byte_enable(cfg_ext_write_byte_enable),
.cfg_ext_read_data(cfg_ext_read_data),
.cfg_ext_read_data_valid(cfg_ext_read_data_valid),
.msix_interrupt(msix_interrupt),
.msix_vector(msix_vector),
.msix_interrupt_ack(msix_interrupt_ack)
);
// Production PCIe TLP processing and DMA engine
logic [31:0] dma_read_addr;
logic [31:0] dma_write_addr;
logic [31:0] dma_length;
logic dma_read_req;
logic dma_write_req;
logic dma_busy;
logic [31:0] dma_read_data;
logic [31:0] dma_write_data;
logic dma_read_valid;
logic dma_write_ready;
// TLP packet parsing state machine
typedef enum logic [2:0] {{
TLP_IDLE,
TLP_HEADER,
TLP_PAYLOAD,
TLP_RESPONSE
}} tlp_state_t;
tlp_state_t tlp_state;
logic [31:0] tlp_header [0:3];
logic [7:0] tlp_header_count;
logic [10:0] tlp_length;
logic [6:0] tlp_type;
logic [31:0] tlp_address;
// PCIe TLP processing engine
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
pcie_tx_data <= 32'h0;
pcie_tx_valid <= 1'b0;
debug_status <= 32'h0;
device_ready <= 1'b0;
tlp_state <= TLP_IDLE;
tlp_header_count <= 8'h0;
dma_read_req <= 1'b0;
dma_write_req <= 1'b0;
dma_read_addr <= 32'h0;
dma_write_addr <= 32'h0;
dma_length <= 32'h0;
end else begin
// Default assignments
pcie_tx_valid <= 1'b0;
dma_read_req <= 1'b0;
dma_write_req <= 1'b0;
case (tlp_state)
TLP_IDLE: begin
if (pcie_rx_valid) begin
tlp_header[0] <= pcie_rx_data;
tlp_header_count <= 8'h1;
tlp_state <= TLP_HEADER;
// Extract TLP type and length from first header
tlp_type <= pcie_rx_data[30:24];
tlp_length <= pcie_rx_data[9:0];
end
device_ready <= 1'b1;
end
TLP_HEADER: begin
if (pcie_rx_valid) begin
tlp_header[tlp_header_count] <= pcie_rx_data;
tlp_header_count <= tlp_header_count + 1;
// For memory requests, capture address from header[1]
if (tlp_header_count == 8'h1) begin
tlp_address <= pcie_rx_data;
end
// Move to payload or response based on TLP type
if (tlp_header_count >= 8'h2) begin
case (tlp_type)
7'b0000000: begin // Memory Read Request
dma_read_addr <= tlp_address;
dma_length <= {{21'h0, tlp_length}};
dma_read_req <= 1'b1;
tlp_state <= TLP_RESPONSE;
end
7'b1000000: begin // Memory Write Request
dma_write_addr <= tlp_address;
dma_length <= {{21'h0, tlp_length}};
tlp_state <= TLP_PAYLOAD;
end
default: begin
tlp_state <= TLP_IDLE;
end
endcase
end
end
end
TLP_PAYLOAD: begin
if (pcie_rx_valid && dma_write_ready) begin
dma_write_data <= pcie_rx_data;
dma_write_req <= 1'b1;
if (dma_length <= 32'h1) begin
tlp_state <= TLP_IDLE;
end else begin
dma_length <= dma_length - 1;
dma_write_addr <= dma_write_addr + 4;
end
end
end
TLP_RESPONSE: begin
if (dma_read_valid && !dma_busy) begin
// Send completion TLP with read data
pcie_tx_data <= dma_read_data;
pcie_tx_valid <= 1'b1;
if (dma_length <= 32'h1) begin
tlp_state <= TLP_IDLE;
end else begin
dma_length <= dma_length - 1;
dma_read_addr <= dma_read_addr + 4;
end
end
end
endcase
// Update debug status with device ID and current state
debug_status <= {{16'h{device_info['vendor_id']}, 8'h{device_info['device_id'][2:]}, 5'h0, tlp_state}};
end
end
// DMA engine instance (simplified interface)
// In production, this would connect to actual memory controller
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
dma_read_data <= 32'h0;
dma_read_valid <= 1'b0;
dma_write_ready <= 1'b1;
dma_busy <= 1'b0;
end else begin
// Simulate DMA operations
dma_read_valid <= dma_read_req;
dma_write_ready <= !dma_write_req;
dma_busy <= dma_read_req || dma_write_req;
// Generate realistic read data based on address
if (dma_read_req) begin
dma_read_data <= dma_read_addr ^ 32'hDEADBEEF;
end
end
end
endmodule
"""
def _generate_device_tcl_script(self, device_info: Dict[str, Any]) -> str:
"""Generate device-specific TCL script using build step outputs."""
# Determine FPGA part based on board
board_parts = {
"pcileech_35t325_x4": "xc7a35tcsg324-2",
"pcileech_75t": "xc7a75tfgg484-2",
"pcileech_100t": "xczu3eg-sbva484-1-e",
}
fpga_part = board_parts.get(self.board, "xc7a35tcsg324-2")
# Get device-specific parameters
vendor_id = device_info["vendor_id"]
device_id = device_info["device_id"]
class_code = device_info["class_code"]
revision_id = device_info["revision_id"]
# Generate clean TCL script with device-specific configuration
tcl_content = f'''#==============================================================================
# PCILeech Firmware Build Script
# Generated for device {vendor_id}:{device_id} (Class: {class_code})
# Board: {self.board}
# FPGA Part: {fpga_part}
# Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}
#==============================================================================
# Set up build environment
set project_name "pcileech_firmware"
set project_dir "./vivado_project"
set output_dir "."
# Create project directory
file mkdir $project_dir
puts "Creating Vivado project for {self.board}..."
puts "Device: {vendor_id}:{device_id} (Class: {class_code})"
# Create project with correct FPGA part
create_project $project_name $project_dir -part {fpga_part} -force
# Set project properties
set_property target_language Verilog [current_project]
set_property simulator_language Mixed [current_project]
set_property default_lib xil_defaultlib [current_project]
#==============================================================================
# PCIe IP Core Configuration
#==============================================================================
puts "Creating PCIe IP core for device {vendor_id}:{device_id}..."
puts "FPGA Part: {fpga_part}"
puts "Board: {self.board}"
'''
# Generate appropriate PCIe IP configuration based on FPGA family
if "xc7a35t" in fpga_part:
# For Artix-7 35T, use AXI PCIe IP core which is available for smaller parts
pcie_config = self._generate_axi_pcie_config(vendor_id, device_id, revision_id)
elif "xc7a75t" in fpga_part or "xc7k" in fpga_part:
# For Kintex-7 and larger Artix-7 parts, use pcie_7x IP core
pcie_config = self._generate_pcie_7x_config(vendor_id, device_id, revision_id)
elif "xczu" in fpga_part:
# For Zynq UltraScale+, use PCIe UltraScale IP core
pcie_config = self._generate_pcie_ultrascale_config(vendor_id, device_id, revision_id)
else:
# Default fallback to pcie_7x for unknown parts
pcie_config = self._generate_pcie_7x_config(vendor_id, device_id, revision_id)
tcl_content += f'''
{pcie_config}
#==============================================================================
# Source File Management
#==============================================================================
puts "Adding source files..."
# Add all SystemVerilog files
set sv_files [glob -nocomplain *.sv]
if {{[llength $sv_files] > 0}} {{
puts "Found [llength $sv_files] SystemVerilog files"
add_files -norecurse $sv_files
set_property file_type SystemVerilog [get_files *.sv]
foreach sv_file $sv_files {{
puts " - $sv_file"
}}
}}
# Add all Verilog files
set v_files [glob -nocomplain *.v]
if {{[llength $v_files] > 0}} {{
puts "Found [llength $v_files] Verilog files"
add_files -norecurse $v_files
foreach v_file $v_files {{
puts " - $v_file"
}}
}}
# Add all constraint files
set xdc_files [glob -nocomplain *.xdc]
if {{[llength $xdc_files] > 0}} {{
puts "Found [llength $xdc_files] constraint files"
add_files -fileset constrs_1 -norecurse $xdc_files
foreach xdc_file $xdc_files {{
puts " - $xdc_file"
}}
}}
# Set top module
set top_module ""
if {{[file exists "pcileech_top.sv"]}} {{
set top_module "pcileech_top"
}} elseif {{[file exists "pcileech_tlps128_bar_controller.sv"]}} {{
set top_module "pcileech_tlps128_bar_controller"
}} else {{
set top_files [glob -nocomplain "*top*.sv"]
if {{[llength $top_files] > 0}} {{
set top_file [lindex $top_files 0]
set top_module [file rootname [file tail $top_file]]
}} else {{
puts "ERROR: No suitable top module found!"
exit 1
}}
}}
if {{$top_module != ""}} {{
set_property top $top_module [current_fileset]
puts "Set top module: $top_module"
}} else {{
puts "ERROR: Failed to determine top module"
exit 1
}}
#==============================================================================
# Device-Specific Timing Constraints
#==============================================================================
puts "Adding device-specific timing constraints..."
set timing_constraints {{
# Clock constraints
create_clock -period 10.000 -name sys_clk [get_ports clk]
set_input_delay -clock sys_clk 2.000 [get_ports {{reset_n pcie_rx_*}}]
set_output_delay -clock sys_clk 2.000 [get_ports {{pcie_tx_* msix_* debug_* device_ready}}]
# Device-specific constraints for {vendor_id}:{device_id}
# Board-specific pin assignments for {self.board}
set_property PACKAGE_PIN E3 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property PACKAGE_PIN C12 [get_ports reset_n]
set_property IOSTANDARD LVCMOS33 [get_ports reset_n]
}}
# Write timing constraints to file
set constraints_file "$project_dir/device_constraints.xdc"
set fp [open $constraints_file w]
puts $fp $timing_constraints
close $fp
add_files -fileset constrs_1 -norecurse $constraints_file
#==============================================================================
# Synthesis & Implementation
#==============================================================================
puts "Configuring synthesis settings..."
set_property strategy "Vivado Synthesis Defaults" [get_runs synth_1]
set_property steps.synth_design.args.directive "AreaOptimized_high" [get_runs synth_1]
puts "Starting synthesis..."
reset_run synth_1
launch_runs synth_1 -jobs 8
wait_on_run synth_1
if {{[get_property PROGRESS [get_runs synth_1]] != "100%"}} {{
puts "ERROR: Synthesis failed!"
exit 1
}}
puts "Synthesis completed successfully"
report_utilization -file utilization_synth.rpt
puts "Configuring implementation settings..."
set_property strategy "Performance_Explore" [get_runs impl_1]
puts "Starting implementation..."
launch_runs impl_1 -jobs 8
wait_on_run impl_1
if {{[get_property PROGRESS [get_runs impl_1]] != "100%"}} {{
puts "ERROR: Implementation failed!"
exit 1
}}
puts "Implementation completed successfully"
#==============================================================================
# Report Generation & Bitstream
#==============================================================================
puts "Generating reports..."
open_run impl_1
report_timing_summary -file timing_summary.rpt
report_utilization -file utilization_impl.rpt
report_power -file power_analysis.rpt
report_drc -file drc_report.rpt
puts "Generating bitstream..."
launch_runs impl_1 -to_step write_bitstream -jobs 8
wait_on_run impl_1
# Check bitstream generation
set bitstream_file "$project_dir/$project_name.runs/impl_1/[get_property top [current_fileset]].bit"
if {{[file exists $bitstream_file]}} {{
set output_bit "pcileech_{vendor_id}_{device_id}_{self.board}.bit"
file copy -force $bitstream_file $output_bit
puts "SUCCESS: Bitstream generated successfully!"
puts "Output file: $output_bit"
# Generate additional files
write_cfgmem -format mcs -size 16 -interface SPIx4 \\
-loadbit "up 0x0 $output_bit" \\
-file "pcileech_{vendor_id}_{device_id}_{self.board}.mcs"
if {{[llength [get_debug_cores]] > 0}} {{
write_debug_probes -file "pcileech_{vendor_id}_{device_id}_{self.board}.ltx"
}}
write_checkpoint -force "pcileech_{vendor_id}_{device_id}_{self.board}.dcp"
puts "Generated files:"
puts " - Bitstream: pcileech_{vendor_id}_{device_id}_{self.board}.bit"
puts " - MCS file: pcileech_{vendor_id}_{device_id}_{self.board}.mcs"
puts " - Checkpoint: pcileech_{vendor_id}_{device_id}_{self.board}.dcp"
puts " - Reports: *.rpt"
}} else {{
puts "ERROR: Bitstream generation failed!"
exit 1
}}
puts "Build completed successfully!"
close_project
'''
return tcl_content
def _generate_separate_tcl_files(self, device_info: Dict[str, Any]) -> List[str]:
"""Generate separate TCL files for different build components."""
tcl_files = []
# Generate project setup TCL
project_tcl = self._generate_project_setup_tcl(device_info)
project_path = self.output_dir / "01_project_setup.tcl"
with open(project_path, "w") as f:
f.write(project_tcl)
tcl_files.append(str(project_path))
logger.info("Generated project setup TCL")
# Generate IP core configuration TCL
ip_tcl = self._generate_ip_config_tcl(device_info)
ip_path = self.output_dir / "02_ip_config.tcl"
with open(ip_path, "w") as f:
f.write(ip_tcl)
tcl_files.append(str(ip_path))
logger.info("Generated IP configuration TCL")
# Generate source file management TCL
sources_tcl = self._generate_sources_tcl(device_info)
sources_path = self.output_dir / "03_add_sources.tcl"
with open(sources_path, "w") as f:
f.write(sources_tcl)
tcl_files.append(str(sources_path))
logger.info("Generated sources management TCL")
# Generate constraints TCL
constraints_tcl = self._generate_constraints_tcl(device_info)
constraints_path = self.output_dir / "04_constraints.tcl"
with open(constraints_path, "w") as f:
f.write(constraints_tcl)
tcl_files.append(str(constraints_path))
logger.info("Generated constraints TCL")
# Generate synthesis TCL
synth_tcl = self._generate_synthesis_tcl(device_info)
synth_path = self.output_dir / "05_synthesis.tcl"
with open(synth_path, "w") as f:
f.write(synth_tcl)
tcl_files.append(str(synth_path))
logger.info("Generated synthesis TCL")
# Generate implementation TCL
impl_tcl = self._generate_implementation_tcl(device_info)
impl_path = self.output_dir / "06_implementation.tcl"
with open(impl_path, "w") as f:
f.write(impl_tcl)
tcl_files.append(str(impl_path))
logger.info("Generated implementation TCL")
# Generate bitstream generation TCL
bitstream_tcl = self._generate_bitstream_tcl(device_info)
bitstream_path = self.output_dir / "07_bitstream.tcl"
with open(bitstream_path, "w") as f:
f.write(bitstream_tcl)
tcl_files.append(str(bitstream_path))
logger.info("Generated bitstream TCL")
# Generate master build script that sources all others
master_tcl = self._generate_master_build_tcl(device_info)
master_path = self.output_dir / "build_all.tcl"
with open(master_path, "w") as f:
f.write(master_tcl)
tcl_files.append(str(master_path))
logger.info("Generated master build TCL")
return tcl_files
def _generate_project_setup_tcl(self, device_info: Dict[str, Any]) -> str:
"""Generate project setup TCL script."""
board_parts = {
"pcileech_35t325_x4": "xc7a35tcsg324-2",
"pcileech_75t": "xc7a75tfgg484-2",
"pcileech_100t": "xczu3eg-sbva484-1-e",
}
fpga_part = board_parts.get(self.board, "xc7a35tcsg324-2")
vendor_id = device_info["vendor_id"]
device_id = device_info["device_id"]
class_code = device_info["class_code"]
return f'''#==============================================================================
# Project Setup - PCILeech Firmware Build
# Generated for device {vendor_id}:{device_id} (Class: {class_code})
# Board: {self.board}
# FPGA Part: {fpga_part}
# Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}
#==============================================================================
# Set up build environment
set project_name "pcileech_firmware"
set project_dir "./vivado_project"
set output_dir "."
# Create project directory
file mkdir $project_dir
puts "Creating Vivado project for {self.board}..."
puts "Device: {vendor_id}:{device_id} (Class: {class_code})"
# Create project with correct FPGA part
create_project $project_name $project_dir -part {fpga_part} -force
# Set project properties
set_property target_language Verilog [current_project]
set_property simulator_language Mixed [current_project]
set_property default_lib xil_defaultlib [current_project]
puts "Project setup completed successfully"
'''
def _generate_ip_config_tcl(self, device_info: Dict[str, Any]) -> str:
"""Generate IP core configuration TCL script."""
vendor_id = device_info["vendor_id"]
device_id = device_info["device_id"]
revision_id = device_info["revision_id"]
# Determine FPGA part based on board
board_parts = {
"pcileech_35t325_x4": "xc7a35tcsg324-2",
"pcileech_75t": "xc7a75tfgg484-2",
"pcileech_100t": "xczu3eg-sbva484-1-e",
}
fpga_part = board_parts.get(self.board, "xc7a35tcsg324-2")
# Generate appropriate PCIe IP configuration based on FPGA family
if "xczu" in fpga_part:
# For Zynq UltraScale+, use PCIe UltraScale IP core
pcie_config = self._generate_pcie_ultrascale_config(vendor_id, device_id, revision_id)
elif "xc7a35t" in fpga_part:
# For Artix-7 35T, use custom implementation (no IP cores)
pcie_config = self._generate_axi_pcie_config(vendor_id, device_id, revision_id)
else:
# For larger 7-series parts, use pcie_7x IP core
pcie_config = self._generate_pcie_7x_config(vendor_id, device_id, revision_id)
return f'''#==============================================================================
# IP Core Configuration - PCIe Core Setup
# Device: {vendor_id}:{device_id}
# FPGA Part: {fpga_part}
# Board: {self.board}
#==============================================================================
puts "Creating PCIe IP core for device {vendor_id}:{device_id}..."
puts "FPGA Part: {fpga_part}"
puts "Board: {self.board}"
{pcie_config}
puts "PCIe IP core configuration completed"
'''
def _generate_axi_pcie_config(self, vendor_id: str, device_id: str, revision_id: str) -> str:
"""Generate custom PCIe configuration for Artix-7 35T parts (no IP cores needed)."""
return f'''# Artix-7 35T PCIe Configuration
# This part uses custom SystemVerilog modules instead of Xilinx IP cores
# Device configuration: {vendor_id}:{device_id} (Rev: {revision_id})
# Set device-specific parameters for custom PCIe implementation
set DEVICE_ID 0x{device_id}
set VENDOR_ID 0x{vendor_id}
set REVISION_ID 0x{revision_id}
set SUBSYSTEM_VENDOR_ID 0x{vendor_id}
set SUBSYSTEM_ID 0x0000
puts "Using custom PCIe implementation for Artix-7 35T"
puts "Device ID: $DEVICE_ID"
puts "Vendor ID: $VENDOR_ID"
puts "Revision ID: $REVISION_ID"
# No IP cores required - PCIe functionality implemented in custom SystemVerilog modules'''
def _generate_pcie_7x_config(self, vendor_id: str, device_id: str, revision_id: str) -> str:
"""Generate PCIe 7-series IP configuration for Kintex-7 and larger parts."""
return f'''# Create PCIe 7-series IP core
create_ip -name pcie_7x -vendor xilinx.com -library ip -module_name pcie_7x_0
# Configure PCIe IP core with device-specific settings
set_property -dict [list \\
CONFIG.Bar0_Scale {{Kilobytes}} \\
CONFIG.Bar0_Size {{128_KB}} \\
CONFIG.Device_ID {{0x{device_id}}} \\
CONFIG.Vendor_ID {{0x{vendor_id}}} \\
CONFIG.Subsystem_Vendor_ID {{0x{vendor_id}}} \\
CONFIG.Subsystem_ID {{0x0000}} \\
CONFIG.Revision_ID {{0x{revision_id}}} \\
CONFIG.Link_Speed {{2.5_GT/s}} \\
CONFIG.Max_Link_Width {{X1}} \\
CONFIG.Maximum_Link_Width {{X1}} \\
CONFIG.Enable_Slot_Clock_Configuration {{false}} \\
CONFIG.Legacy_Interrupt {{NONE}} \\
CONFIG.MSI_Enabled {{false}} \\
CONFIG.MSI_64b_Address_Capable {{false}} \\
CONFIG.MSIX_Enabled {{true}} \\
] [get_ips pcie_7x_0]'''
def _generate_pcie_ultrascale_config(self, vendor_id: str, device_id: str, revision_id: str) -> str:
"""Generate PCIe UltraScale IP configuration for Zynq UltraScale+ parts."""
return f'''# Create PCIe UltraScale IP core
create_ip -name pcie4_uscale_plus -vendor xilinx.com -library ip -module_name pcie4_uscale_plus_0
# Configure PCIe UltraScale IP core with device-specific settings
set_property -dict [list \\
CONFIG.PL_LINK_CAP_MAX_LINK_SPEED {{2.5_GT/s}} \\
CONFIG.PL_LINK_CAP_MAX_LINK_WIDTH {{X1}} \\
CONFIG.AXISTEN_IF_EXT_512_RQ_STRADDLE {{false}} \\
CONFIG.PF0_DEVICE_ID {{0x{device_id}}} \\
CONFIG.PF0_VENDOR_ID {{0x{vendor_id}}} \\
CONFIG.PF0_SUBSYSTEM_VENDOR_ID {{0x{vendor_id}}} \\
CONFIG.PF0_SUBSYSTEM_ID {{0x0000}} \\
CONFIG.PF0_REVISION_ID {{0x{revision_id}}} \\
CONFIG.PF0_CLASS_CODE {{0x040300}} \\
CONFIG.PF0_BAR0_SCALE {{Kilobytes}} \\
CONFIG.PF0_BAR0_SIZE {{128}} \\
CONFIG.PF0_MSI_ENABLED {{false}} \\
CONFIG.PF0_MSIX_ENABLED {{true}} \\
] [get_ips pcie4_uscale_plus_0]'''
def _generate_sources_tcl(self, device_info: Dict[str, Any]) -> str:
"""Generate source file management TCL script."""
return '''#==============================================================================
# Source File Management
#==============================================================================
puts "Adding source files..."
# Add all SystemVerilog files
set sv_files [glob -nocomplain *.sv]
if {[llength $sv_files] > 0} {
puts "Found [llength $sv_files] SystemVerilog files"
add_files -norecurse $sv_files
set_property file_type SystemVerilog [get_files *.sv]
foreach sv_file $sv_files {
puts " - $sv_file"
}
}
# Add all Verilog files
set v_files [glob -nocomplain *.v]
if {[llength $v_files] > 0} {
puts "Found [llength $v_files] Verilog files"
add_files -norecurse $v_files
foreach v_file $v_files {
puts " - $v_file"
}
}
# Set top module
set top_module ""
if {[file exists "pcileech_top.sv"]} {
set top_module "pcileech_top"
} elseif {[file exists "pcileech_tlps128_bar_controller.sv"]} {
set top_module "pcileech_tlps128_bar_controller"
} else {
set top_files [glob -nocomplain "*top*.sv"]
if {[llength $top_files] > 0} {
set top_file [lindex $top_files 0]
set top_module [file rootname [file tail $top_file]]
} else {
puts "ERROR: No suitable top module found!"
exit 1
}
}
if {$top_module != ""} {
set_property top $top_module [current_fileset]
puts "Set top module: $top_module"
} else {
puts "ERROR: Failed to determine top module"
exit 1
}
puts "Source file management completed"
'''
def _generate_constraints_tcl(self, device_info: Dict[str, Any]) -> str:
"""Generate constraints TCL script."""
vendor_id = device_info["vendor_id"]
device_id = device_info["device_id"]
return f'''#==============================================================================
# Constraints Management
# Device: {vendor_id}:{device_id}
# Board: {self.board}
#==============================================================================
puts "Adding constraint files..."
# Add all constraint files
set xdc_files [glob -nocomplain *.xdc]
if {{[llength $xdc_files] > 0}} {{
puts "Found [llength $xdc_files] constraint files"
add_files -fileset constrs_1 -norecurse $xdc_files
foreach xdc_file $xdc_files {{
puts " - $xdc_file"
}}
}}
# Generate device-specific timing constraints
puts "Adding device-specific timing constraints..."
set timing_constraints {{
# Clock constraints
create_clock -period 10.000 -name sys_clk [get_ports clk]
set_input_delay -clock sys_clk 2.000 [get_ports {{reset_n pcie_rx_*}}]
set_output_delay -clock sys_clk 2.000 [get_ports {{pcie_tx_* msix_* debug_* device_ready}}]
# Device-specific constraints for {vendor_id}:{device_id}
# Board-specific pin assignments for {self.board}
set_property PACKAGE_PIN E3 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property PACKAGE_PIN C12 [get_ports reset_n]
set_property IOSTANDARD LVCMOS33 [get_ports reset_n]
}}
# Write timing constraints to file
set constraints_file "$project_dir/device_constraints.xdc"
set fp [open $constraints_file w]
puts $fp $timing_constraints
close $fp
add_files -fileset constrs_1 -norecurse $constraints_file
puts "Constraints setup completed"
'''
def _generate_synthesis_tcl(self, device_info: Dict[str, Any]) -> str:
"""Generate synthesis TCL script."""
return '''#==============================================================================
# Synthesis Configuration and Execution
#==============================================================================
puts "Configuring synthesis settings..."
set_property strategy "Vivado Synthesis Defaults" [get_runs synth_1]
set_property steps.synth_design.args.directive "AreaOptimized_high" [get_runs synth_1]
puts "Starting synthesis..."
reset_run synth_1
launch_runs synth_1 -jobs 8
wait_on_run synth_1
if {[get_property PROGRESS [get_runs synth_1]] != "100%"} {
puts "ERROR: Synthesis failed!"
exit 1
}
puts "Synthesis completed successfully"
report_utilization -file utilization_synth.rpt
'''
def _generate_implementation_tcl(self, device_info: Dict[str, Any]) -> str:
"""Generate implementation TCL script."""
return '''#==============================================================================
# Implementation Configuration and Execution
#==============================================================================
puts "Configuring implementation settings..."
set_property strategy "Performance_Explore" [get_runs impl_1]
puts "Starting implementation..."
launch_runs impl_1 -jobs 8
wait_on_run impl_1
if {[get_property PROGRESS [get_runs impl_1]] != "100%"} {
puts "ERROR: Implementation failed!"
exit 1
}
puts "Implementation completed successfully"
# Generate implementation reports
puts "Generating reports..."
open_run impl_1
report_timing_summary -file timing_summary.rpt
report_utilization -file utilization_impl.rpt
report_power -file power_analysis.rpt
report_drc -file drc_report.rpt
'''
def _generate_bitstream_tcl(self, device_info: Dict[str, Any]) -> str:
"""Generate bitstream generation TCL script."""
vendor_id = device_info["vendor_id"]
device_id = device_info["device_id"]
return f'''#==============================================================================
# Bitstream Generation
# Device: {vendor_id}:{device_id}
# Board: {self.board}
#==============================================================================
puts "Generating bitstream..."
launch_runs impl_1 -to_step write_bitstream -jobs 8
wait_on_run impl_1
# Check bitstream generation
set bitstream_file "$project_dir/$project_name.runs/impl_1/[get_property top [current_fileset]].bit"
if {{[file exists $bitstream_file]}} {{
set output_bit "pcileech_{vendor_id}_{device_id}_{self.board}.bit"
file copy -force $bitstream_file $output_bit
puts "SUCCESS: Bitstream generated successfully!"
puts "Output file: $output_bit"
# Generate additional files
write_cfgmem -format mcs -size 16 -interface SPIx4 \\
-loadbit "up 0x0 $output_bit" \\
-file "pcileech_{vendor_id}_{device_id}_{self.board}.mcs"
if {{[llength [get_debug_cores]] > 0}} {{
write_debug_probes -file "pcileech_{vendor_id}_{device_id}_{self.board}.ltx"
}}
write_checkpoint -force "pcileech_{vendor_id}_{device_id}_{self.board}.dcp"
puts "Generated files:"
puts " - Bitstream: pcileech_{vendor_id}_{device_id}_{self.board}.bit"
puts " - MCS file: pcileech_{vendor_id}_{device_id}_{self.board}.mcs"
puts " - Checkpoint: pcileech_{vendor_id}_{device_id}_{self.board}.dcp"
puts " - Reports: *.rpt"
}} else {{
puts "ERROR: Bitstream generation failed!"
exit 1
}}
puts "Bitstream generation completed successfully!"
'''
def _generate_master_build_tcl(self, device_info: Dict[str, Any]) -> str:
"""Generate master build script that sources all other TCL files."""
vendor_id = device_info["vendor_id"]
device_id = device_info["device_id"]
class_code = device_info["class_code"]
return f'''#==============================================================================
# Master Build Script - PCILeech Firmware
# Generated for device {vendor_id}:{device_id} (Class: {class_code})
# Board: {self.board}
# Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}
#==============================================================================
puts "Starting PCILeech firmware build process..."
puts "Device: {vendor_id}:{device_id} (Class: {class_code})"
puts "Board: {self.board}"
puts ""
# Source all build scripts in order
set build_scripts [list \\
"01_project_setup.tcl" \\
"02_ip_config.tcl" \\
"03_add_sources.tcl" \\
"04_constraints.tcl" \\
"05_synthesis.tcl" \\
"06_implementation.tcl" \\
"07_bitstream.tcl" \\
]
foreach script $build_scripts {{
if {{[file exists $script]}} {{
puts "Executing: $script"
source $script
puts "Completed: $script"
puts ""
}} else {{
puts "ERROR: Required script not found: $script"
exit 1
}}
}}
puts "Build process completed successfully!"
close_project
'''
def _apply_manufacturing_variance(self, device_info: Dict[str, Any]) -> List[str]:
"""Apply manufacturing variance simulation."""
variance_files = []
try:
if not DeviceClass or not VarianceModel:
logger.warning("Manufacturing variance modules not available")
return variance_files
# Determine device class based on actual enum values
class_code = int(device_info["class_code"], 16)
if class_code == 0x0200: # Ethernet
device_class = DeviceClass.ENTERPRISE
elif class_code == 0x0403: # Audio
device_class = DeviceClass.CONSUMER
else:
device_class = DeviceClass.CONSUMER
# Create variance model
variance_model = VarianceModel(
device_id=device_info["device_id"],
device_class=device_class,
base_frequency_mhz=100.0, # Default frequency
clock_jitter_percent=2.5,
register_timing_jitter_ns=25.0,
power_noise_percent=2.0,
temperature_drift_ppm_per_c=50.0,
process_variation_percent=10.0,
propagation_delay_ps=100.0,
)
# Save variance data
variance_data = {
"device_class": device_class.value,
"variance_model": {
"device_id": variance_model.device_id,
"device_class": variance_model.device_class.value,
"base_frequency_mhz": variance_model.base_frequency_mhz,
"clock_jitter_percent": variance_model.clock_jitter_percent,
"register_timing_jitter_ns": variance_model.register_timing_jitter_ns,
"power_noise_percent": variance_model.power_noise_percent,
"temperature_drift_ppm_per_c": variance_model.temperature_drift_ppm_per_c,
"process_variation_percent": variance_model.process_variation_percent,
"propagation_delay_ps": variance_model.propagation_delay_ps,
},
}
variance_file = self.output_dir / "manufacturing_variance.json"
with open(variance_file, "w") as f:
json.dump(variance_data, f, indent=2)
variance_files.append(str(variance_file))
logger.info(f"Applied manufacturing variance for {device_class.value}")
except Exception as e:
logger.error(f"Error applying manufacturing variance: {e}")
return variance_files
def run_behavior_profiling(
self, device_info: Dict[str, Any], duration: int = 30
) -> Optional[str]:
"""Run behavior profiling if available."""
if not BehaviorProfiler:
logger.warning("Behavior profiler not available")
return None
try:
logger.info(f"Starting behavior profiling for {duration} seconds")
self.behavior_profiler = BehaviorProfiler(self.bdf)
# Capture behavior profile
profile_data = self.behavior_profiler.capture_behavior_profile(duration)
# Convert to serializable format
profile_dict = {
"device_bdf": profile_data.device_bdf,
"capture_duration": profile_data.capture_duration,
"total_accesses": profile_data.total_accesses,
"register_accesses": [
{
"timestamp": access.timestamp,
"register": access.register,
"offset": access.offset,
"operation": access.operation,
"value": access.value,
"duration_us": access.duration_us,
}
for access in profile_data.register_accesses
],
"timing_patterns": [
{
"pattern_type": pattern.pattern_type,
"registers": pattern.registers,
"avg_interval_us": pattern.avg_interval_us,
"std_deviation_us": pattern.std_deviation_us,
"frequency_hz": pattern.frequency_hz,
"confidence": pattern.confidence,
}
for pattern in profile_data.timing_patterns
],
"state_transitions": profile_data.state_transitions,
"power_states": profile_data.power_states,
"interrupt_patterns": profile_data.interrupt_patterns,
}
# Save profile data
profile_file = self.output_dir / "behavior_profile.json"
with open(profile_file, "w") as f:
json.dump(profile_dict, f, indent=2)
logger.info(f"Behavior profiling completed, saved to {profile_file}")
return str(profile_file)
except Exception as e:
logger.error(f"Error during behavior profiling: {e}")
return None
def generate_build_files(self, device_info: Dict[str, Any]) -> List[str]:
"""Generate separate build files (TCL scripts, makefiles, etc.)."""
build_files = []
# Clean up any old unified TCL files first
old_unified_files = [
self.output_dir / "build_unified.tcl",
self.output_dir / "unified_build.tcl",
self.output_dir / "build_firmware.tcl", # Remove the old monolithic file too
]
for old_file in old_unified_files:
if old_file.exists():
old_file.unlink()
logger.info(f"Removed old unified file: {old_file.name}")
# Generate separate TCL files for different components
tcl_files = self._generate_separate_tcl_files(device_info)
build_files.extend(tcl_files)
# Generate project file
project_file = self._generate_project_file(device_info)
proj_file = self.output_dir / "firmware_project.json"
with open(proj_file, "w") as f:
json.dump(project_file, f, indent=2)
build_files.append(str(proj_file))
# Generate file manifest for verification
manifest = self._generate_file_manifest(device_info)
manifest_file = self.output_dir / "file_manifest.json"
with open(manifest_file, "w") as f:
json.dump(manifest, f, indent=2)
build_files.append(str(manifest_file))
logger.info(f"Generated {len(build_files)} build files")
return build_files
def _generate_project_file(self, device_info: Dict[str, Any]) -> Dict[str, Any]:
"""Generate project configuration file."""
return {
"project_name": "pcileech_firmware",
"board": self.board,
"device_info": device_info,
"build_timestamp": time.time(),
"build_version": "1.0.0",
"features": {
"advanced_sv": hasattr(self, "sv_generator")
and self.sv_generator is not None,
"manufacturing_variance": hasattr(self, "variance_simulator")
and self.variance_simulator is not None,
"behavior_profiling": hasattr(self, "behavior_profiler")
and self.behavior_profiler is not None,
},
}
def _generate_file_manifest(self, device_info: Dict[str, Any]) -> Dict[str, Any]:
"""Generate a manifest of all files for verification."""
manifest = {
"project_info": {
"device": f"{device_info['vendor_id']}:{device_info['device_id']}",
"board": self.board,
"generated_at": time.strftime('%Y-%m-%d %H:%M:%S'),
},
"files": {
"systemverilog": [],
"verilog": [],
"constraints": [],
"tcl_scripts": [],
"generated": [],
},
"validation": {
"required_files_present": True,
"top_module_identified": False,
"build_script_ready": False,
}
}
# Check for files in output directory
output_files = list(self.output_dir.glob("*"))
for file_path in output_files:
if file_path.suffix == ".sv":
manifest["files"]["systemverilog"].append(file_path.name)
if "top" in file_path.name.lower():
manifest["validation"]["top_module_identified"] = True
elif file_path.suffix == ".v":
manifest["files"]["verilog"].append(file_path.name)
elif file_path.suffix == ".xdc":
manifest["files"]["constraints"].append(file_path.name)
elif file_path.suffix == ".tcl":
manifest["files"]["tcl_scripts"].append(file_path.name)
if "build" in file_path.name:
manifest["validation"]["build_script_ready"] = True
elif file_path.suffix == ".json":
manifest["files"]["generated"].append(file_path.name)
# Validate required files
required_files = ["device_config.sv", "pcileech_top.sv"]
manifest["validation"]["required_files_present"] = all(
f in manifest["files"]["systemverilog"] for f in required_files
)
return manifest
def _cleanup_intermediate_files(self) -> List[str]:
"""Clean up intermediate files, keeping only final outputs and logs."""
preserved_files = []
cleaned_files = []
# Define patterns for files to preserve
preserve_patterns = [
"*.bit", # Final bitstream
"*.mcs", # Flash memory file
"*.ltx", # Debug probes
"*.dcp", # Design checkpoint
"*.log", # Log files
"*.rpt", # Report files
"build_firmware.tcl", # Final TCL build script
"*.tcl", # All TCL files (preserve in-place)
"*.sv", # SystemVerilog source files (needed for build)
"*.v", # Verilog source files (needed for build)
"*.xdc", # Constraint files (needed for build)
]
# Define patterns for files/directories to clean
cleanup_patterns = [
"vivado_project/", # Vivado project directory
"project_dir/", # Alternative project directory
"*.json", # JSON files (intermediate)
"*.jou", # Vivado journal files
"*.str", # Vivado strategy files
".Xil/", # Xilinx temporary directory
]
logger.info("Starting cleanup of intermediate files...")
try:
import shutil
import fnmatch
# Get all files in output directory
all_files = list(self.output_dir.rglob("*"))
for file_path in all_files:
should_preserve = False
# Check if file should be preserved
for pattern in preserve_patterns:
if fnmatch.fnmatch(file_path.name, pattern):
should_preserve = True
preserved_files.append(str(file_path))
break
# If not preserved, check if it should be cleaned
if not should_preserve:
# Handle cleanup patterns
for pattern in cleanup_patterns:
if pattern.endswith("/"):
# Directory pattern
if file_path.is_dir() and fnmatch.fnmatch(file_path.name + "/", pattern):
try:
shutil.rmtree(file_path)
cleaned_files.append(str(file_path))
logger.info(f"Cleaned directory: {file_path.name}")
except Exception as e:
logger.warning(f"Could not clean directory {file_path.name}: {e}")
break
else:
# File pattern
if file_path.is_file() and fnmatch.fnmatch(file_path.name, pattern):
try:
file_path.unlink()
cleaned_files.append(str(file_path))
logger.debug(f"Cleaned file: {file_path.name}")
except Exception as e:
logger.warning(f"Could not clean file {file_path.name}: {e}")
break
logger.info(f"Cleanup completed: preserved {len(preserved_files)} files, cleaned {len(cleaned_files)} items")
except Exception as e:
logger.error(f"Error during cleanup: {e}")
return preserved_files
def _validate_final_outputs(self) -> Dict[str, Any]:
"""Validate and provide information about final output files."""
validation_results = {
"bitstream_info": None,
"flash_file_info": None,
"debug_file_info": None,
"tcl_file_info": None,
"reports_info": [],
"validation_status": "unknown",
"file_sizes": {},
"checksums": {},
"build_mode": "unknown",
}
try:
import hashlib
# Check for TCL build file (main output when Vivado not available)
tcl_files = list(self.output_dir.glob("build_firmware.tcl"))
if tcl_files:
tcl_file = tcl_files[0]
file_size = tcl_file.stat().st_size
with open(tcl_file, "r") as f:
content = f.read()
file_hash = hashlib.sha256(content.encode()).hexdigest()
validation_results["tcl_file_info"] = {
"filename": tcl_file.name,
"size_bytes": file_size,
"size_kb": round(file_size / 1024, 2),
"sha256": file_hash,
"has_device_config": "CONFIG.Device_ID" in content,
"has_synthesis": "launch_runs synth_1" in content,
"has_implementation": "launch_runs impl_1" in content,
}
validation_results["file_sizes"][tcl_file.name] = file_size
validation_results["checksums"][tcl_file.name] = file_hash
# Check for bitstream file (only if Vivado was run)
bitstream_files = list(self.output_dir.glob("*.bit"))
if bitstream_files:
bitstream_file = bitstream_files[0]
file_size = bitstream_file.stat().st_size
# Calculate checksum
with open(bitstream_file, "rb") as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
validation_results["bitstream_info"] = {
"filename": bitstream_file.name,
"size_bytes": file_size,
"size_mb": round(file_size / (1024 * 1024), 2),
"sha256": file_hash,
"created": bitstream_file.stat().st_mtime,
}
validation_results["file_sizes"][bitstream_file.name] = file_size
validation_results["checksums"][bitstream_file.name] = file_hash
validation_results["build_mode"] = "full_vivado"
else:
validation_results["build_mode"] = "tcl_only"
# Check for MCS flash file
mcs_files = list(self.output_dir.glob("*.mcs"))
if mcs_files:
mcs_file = mcs_files[0]
file_size = mcs_file.stat().st_size
with open(mcs_file, "rb") as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
validation_results["flash_file_info"] = {
"filename": mcs_file.name,
"size_bytes": file_size,
"size_mb": round(file_size / (1024 * 1024), 2),
"sha256": file_hash,
}
validation_results["file_sizes"][mcs_file.name] = file_size
validation_results["checksums"][mcs_file.name] = file_hash
# Check for debug file
ltx_files = list(self.output_dir.glob("*.ltx"))
if ltx_files:
ltx_file = ltx_files[0]
file_size = ltx_file.stat().st_size
validation_results["debug_file_info"] = {
"filename": ltx_file.name,
"size_bytes": file_size,
}
validation_results["file_sizes"][ltx_file.name] = file_size
# Check for report files
report_files = list(self.output_dir.glob("*.rpt"))
for report_file in report_files:
file_size = report_file.stat().st_size
validation_results["reports_info"].append({
"filename": report_file.name,
"size_bytes": file_size,
"type": self._determine_report_type(report_file.name),
})
validation_results["file_sizes"][report_file.name] = file_size
# Determine overall validation status
if validation_results["tcl_file_info"]:
if validation_results["build_mode"] == "full_vivado":
# Full Vivado build - check bitstream
if validation_results["bitstream_info"]:
if validation_results["bitstream_info"]["size_bytes"] > 1000000: # > 1MB
validation_results["validation_status"] = "success_full_build"
else:
validation_results["validation_status"] = "warning_small_bitstream"
else:
validation_results["validation_status"] = "failed_no_bitstream"
else:
# TCL-only build - check TCL file quality (this is the main output)
tcl_info = validation_results["tcl_file_info"]
if tcl_info["has_device_config"] and tcl_info["size_bytes"] > 1000:
validation_results["validation_status"] = "success_tcl_ready"
else:
validation_results["validation_status"] = "warning_incomplete_tcl"
else:
validation_results["validation_status"] = "failed_no_tcl"
except Exception as e:
logger.error(f"Error during output validation: {e}")
validation_results["validation_status"] = "error"
return validation_results
def _determine_report_type(self, filename: str) -> str:
"""Determine the type of report based on filename."""
if "timing" in filename.lower():
return "timing_analysis"
elif "utilization" in filename.lower():
return "resource_utilization"
elif "power" in filename.lower():
return "power_analysis"
elif "drc" in filename.lower():
return "design_rule_check"
else:
return "general"
def build_firmware(
self,
advanced_sv: bool = False,
device_type: Optional[str] = None,
enable_variance: bool = False,
behavior_profile_duration: int = 30,
) -> Dict[str, Any]:
"""Main firmware build process."""
logger.info("Starting firmware build process")
build_results = {
"success": False,
"files_generated": [],
"errors": [],
"build_time": 0,
}
start_time = time.time()
try:
# Step 1: Read configuration space
logger.info("Step 1: Reading device configuration space")
config_space = self.read_vfio_config_space()
# Step 2: Extract device information
logger.info("Step 2: Extracting device information")
device_info = self.extract_device_info(config_space)
# Step 3: Generate SystemVerilog files
logger.info("Step 3: Generating SystemVerilog files")
sv_files = self.generate_systemverilog_files(
device_info, advanced_sv, device_type, enable_variance
)
build_results["files_generated"].extend(sv_files)
# Step 4: Run behavior profiling if requested
if behavior_profile_duration > 0:
logger.info("Step 4: Running behavior profiling")
profile_file = self.run_behavior_profiling(
device_info, behavior_profile_duration
)
if profile_file:
build_results["files_generated"].append(profile_file)
# Step 5: Generate build files
logger.info("Step 5: Generating build files")
build_files = self.generate_build_files(device_info)
build_results["files_generated"].extend(build_files)
# Step 6: Save device info
device_info_file = self.output_dir / "device_info.json"
with open(device_info_file, "w") as f:
json.dump(device_info, f, indent=2)
build_results["files_generated"].append(str(device_info_file))
# Step 7: Clean up intermediate files
logger.info("Step 7: Cleaning up intermediate files")
preserved_files = self._cleanup_intermediate_files()
# Step 8: Validate final outputs
logger.info("Step 8: Validating final outputs")
validation_results = self._validate_final_outputs()
build_results["success"] = True
build_results["build_time"] = time.time() - start_time
build_results["preserved_files"] = preserved_files
build_results["validation"] = validation_results
logger.info(
f"Firmware build completed successfully in {build_results['build_time']:.2f} seconds"
)
logger.info(f"Generated {len(build_results['files_generated'])} files")
logger.info(f"Preserved {len(preserved_files)} final output files")
# Print detailed validation information
self._print_final_output_info(validation_results)
except Exception as e:
error_msg = f"Build failed: {e}"
logger.error(error_msg)
build_results["errors"].append(error_msg)
build_results["build_time"] = time.time() - start_time
return build_results
def _print_final_output_info(self, validation_results: Dict[str, Any]):
"""Print detailed information about final output files."""
print("\n" + "="*80)
print("FINAL BUILD OUTPUT VALIDATION")
print("="*80)
build_mode = validation_results.get("build_mode", "unknown")
status = validation_results["validation_status"]
# Display build status
if status == "success_full_build":
print("✅ BUILD STATUS: SUCCESS (Full Vivado Build)")
elif status == "success_tcl_ready":
print("✅ BUILD STATUS: SUCCESS (TCL Build Script Ready)")
elif status == "warning_small_bitstream":
print("⚠️ BUILD STATUS: WARNING - Bitstream file is unusually small")
elif status == "warning_incomplete_tcl":
print("⚠️ BUILD STATUS: WARNING - TCL script may be incomplete")
elif status == "failed_no_bitstream":
print("❌ BUILD STATUS: FAILED - No bitstream file generated")
elif status == "failed_no_tcl":
print("❌ BUILD STATUS: FAILED - No TCL build script generated")
else:
print("❌ BUILD STATUS: ERROR - Validation failed")
print(f"\n🔧 BUILD MODE: {build_mode.replace('_', ' ').title()}")
# TCL file information (always show if present)
if validation_results.get("tcl_file_info"):
info = validation_results["tcl_file_info"]
print(f"\n📜 BUILD SCRIPT:")
print(f" File: {info['filename']}")
print(f" Size: {info['size_kb']} KB ({info['size_bytes']:,} bytes)")
print(f" SHA256: {info['sha256'][:16]}...")
# TCL script validation
features = []
if info["has_device_config"]:
features.append("✅ Device-specific configuration")
else:
features.append("❌ Missing device configuration")
if info["has_synthesis"]:
features.append("✅ Synthesis commands")
else:
features.append("⚠️ No synthesis commands")
if info["has_implementation"]:
features.append("✅ Implementation commands")
else:
features.append("⚠️ No implementation commands")
print(" Features:")
for feature in features:
print(f" {feature}")
# Bitstream information (only if Vivado was run)
if validation_results.get("bitstream_info"):
info = validation_results["bitstream_info"]
print(f"\n📁 BITSTREAM FILE:")
print(f" File: {info['filename']}")
print(f" Size: {info['size_mb']} MB ({info['size_bytes']:,} bytes)")
print(f" SHA256: {info['sha256'][:16]}...")
# Validate bitstream size
if info['size_mb'] < 0.5:
print(" ⚠️ WARNING: Bitstream is very small, may be incomplete")
elif info['size_mb'] > 10:
print(" ⚠️ WARNING: Bitstream is very large, check for issues")
else:
print(" ✅ Bitstream size looks normal")
# Flash file information
if validation_results.get("flash_file_info"):
info = validation_results["flash_file_info"]
print(f"\n💾 FLASH FILE:")
print(f" File: {info['filename']}")
print(f" Size: {info['size_mb']} MB ({info['size_bytes']:,} bytes)")
print(f" SHA256: {info['sha256'][:16]}...")
# Debug file information
if validation_results.get("debug_file_info"):
info = validation_results["debug_file_info"]
print(f"\n🔍 DEBUG FILE:")
print(f" File: {info['filename']}")
print(f" Size: {info['size_bytes']:,} bytes")
# Report files
if validation_results.get("reports_info"):
print(f"\n📊 ANALYSIS REPORTS:")
for report in validation_results["reports_info"]:
print(f" {report['filename']} ({report['type']}) - {report['size_bytes']:,} bytes")
# File checksums for verification
if validation_results.get("checksums"):
print(f"\n🔐 FILE CHECKSUMS (for verification):")
for filename, checksum in validation_results["checksums"].items():
print(f" {filename}: {checksum}")
print("\n" + "="*80)
if build_mode == "tcl_only":
print("TCL build script is ready! Run with Vivado to generate bitstream.")
else:
print("Build output files are ready for deployment!")
print("="*80 + "\n")
def main():
"""Main entry point for the build system."""
parser = argparse.ArgumentParser(
description="PCILeech FPGA Firmware Builder - Production System"
)
parser.add_argument(
"--bdf", required=True, help="Bus:Device.Function (e.g., 0000:03:00.0)"
)
parser.add_argument("--board", required=True, help="Target board")
parser.add_argument(
"--advanced-sv",
action="store_true",
help="Enable advanced SystemVerilog generation",
)
parser.add_argument(
"--device-type",
help="Device type for optimizations (network, audio, storage, etc.)",
)
parser.add_argument(
"--enable-variance",
action="store_true",
help="Enable manufacturing variance simulation",
)
parser.add_argument(
"--behavior-profile-duration",
type=int,
default=30,
help="Duration for behavior profiling in seconds (0 to disable)",
)
parser.add_argument("--verbose", action="store_true", help="Verbose output")
args = parser.parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
try:
# Initialize builder
builder = PCILeechFirmwareBuilder(args.bdf, args.board)
# Run build process
results = builder.build_firmware(
advanced_sv=args.advanced_sv,
device_type=args.device_type,
enable_variance=args.enable_variance,
behavior_profile_duration=args.behavior_profile_duration,
)
# Print results
if results["success"]:
print(
f"[✓] Build completed successfully in {results['build_time']:.2f} seconds"
)
# Show preserved files (final outputs)
if "preserved_files" in results and results["preserved_files"]:
print(f"[✓] Final output files ({len(results['preserved_files'])}):")
for file_path in results["preserved_files"]:
print(f" - {file_path}")
# Validation results are already printed by _print_final_output_info
return 0
else:
print(f"[✗] Build failed after {results['build_time']:.2f} seconds")
for error in results["errors"]:
print(f" Error: {error}")
return 1
except KeyboardInterrupt:
print("\n[!] Build interrupted by user")
return 130
except Exception as e:
print(f"[✗] Fatal error: {e}")
logger.exception("Fatal error during build")
return 1
if __name__ == "__main__":
sys.exit(main())